diff --git a/.github/actions/Build/action.yml b/.github/actions/Build/action.yml new file mode 100644 index 00000000..84a99f04 --- /dev/null +++ b/.github/actions/Build/action.yml @@ -0,0 +1,81 @@ +name: Build-PSModule (by PSModule) +description: Build a PowerShell module to the PowerShell Gallery. +author: PSModule +branding: + icon: package + color: gray-dark + +inputs: + Name: + description: Name of the module to process. + required: false + Path: + description: Path to the folder where the modules are located. + required: false + default: src + ModulesOutputPath: + description: Path to the folder where the built modules are outputted. + required: false + default: outputs/modules + DocsOutputPath: + description: Path to the folder where the built docs are outputted. + required: false + default: outputs/docs + ModuleArtifactName: + description: Name of the module artifact to upload. + required: false + default: module + DocsArtifactName: + description: Name of the docs artifact to upload. + required: false + default: docs + Debug: + description: Enable debug output. + required: false + default: 'false' + Verbose: + description: Enable verbose output. + required: false + default: 'false' + Version: + description: Specifies the version of the GitHub module to be installed. The value must be an exact version. + required: false + Prerelease: + description: Allow prerelease versions if available. + required: false + default: 'false' + +runs: + using: composite + steps: + - name: Run Build-PSModule + uses: PSModule/GitHub-Script@v1 + env: + GITHUB_ACTION_INPUT_Name: ${{ inputs.Name }} + GITHUB_ACTION_INPUT_Path: ${{ inputs.Path }} + GITHUB_ACTION_INPUT_ModulesOutputPath: ${{ inputs.ModulesOutputPath }} + GITHUB_ACTION_INPUT_DocsOutputPath: ${{ inputs.DocsOutputPath }} + with: + Debug: ${{ inputs.Debug }} + Prerelease: ${{ inputs.Prerelease }} + Verbose: ${{ inputs.Verbose }} + Version: ${{ inputs.Version }} + Script: | + # Build-PSModule + . "${{ github.action_path }}\scripts\main.ps1" + + - name: Upload module artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.ModuleArtifactName }} + path: ${{ inputs.ModulesOutputPath }} + if-no-files-found: error + retention-days: 1 + + - name: Upload docs artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.DocsArtifactName }} + path: ${{ inputs.DocsOutputPath }} + if-no-files-found: error + retention-days: 1 diff --git a/.github/actions/Build/scripts/helpers/Build-PSModule.ps1 b/.github/actions/Build/scripts/helpers/Build-PSModule.ps1 new file mode 100644 index 00000000..aa63bf3c --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build-PSModule.ps1 @@ -0,0 +1,61 @@ +#Requires -Modules @{ ModuleName = 'GitHub'; ModuleVersion = '0.13.2' } +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Build-PSModule { + <# + .SYNOPSIS + Builds a module. + + .DESCRIPTION + Builds a module. + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', Scope = 'Function', + Justification = 'LogGroup - Scoping affects the variables line of sight.' + )] + param( + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName, + + # Path to the folder where the modules are located. + [Parameter(Mandatory)] + [string] $ModuleSourceFolderPath, + + # Path to the folder where the built modules are outputted. + [Parameter(Mandatory)] + [string] $ModulesOutputFolderPath, + + # Path to the folder where the documentation is outputted. + [Parameter(Mandatory)] + [string] $DocsOutputFolderPath + ) + + LogGroup "Building module [$ModuleName]" { + Write-Host "Source path: [$ModuleSourceFolderPath]" + if (-not (Test-Path -Path $ModuleSourceFolderPath)) { + Write-Error "Source folder not found at [$ModuleSourceFolderPath]" + exit 1 + } + $moduleSourceFolder = Get-Item -Path $ModuleSourceFolderPath + Write-Host "Module source folder: [$moduleSourceFolder]" + + $moduleOutputFolder = New-Item -Path $ModulesOutputFolderPath -Name $ModuleName -ItemType Directory -Force + Write-Host "Module output folder: [$moduleOutputFolder]" + + $docsOutputFolder = New-Item -Path $DocsOutputFolderPath -ItemType Directory -Force + Write-Host "Docs output folder: [$docsOutputFolder]" + } + + Build-PSModuleBase -ModuleName $ModuleName -ModuleSourceFolder $moduleSourceFolder -ModuleOutputFolder $moduleOutputFolder + Build-PSModuleManifest -ModuleName $ModuleName -ModuleOutputFolder $moduleOutputFolder + Build-PSModuleRootModule -ModuleName $ModuleName -ModuleOutputFolder $moduleOutputFolder + Update-PSModuleManifestAliasesToExport -ModuleName $ModuleName -ModuleOutputFolder $moduleOutputFolder + Build-PSModuleDocumentation -ModuleName $ModuleName -ModuleSourceFolder $moduleSourceFolder -DocsOutputFolder $docsOutputFolder + + LogGroup 'Build manifest file - Final Result' { + $outputManifestPath = Join-Path -Path $ModuleOutputFolder -ChildPath "$ModuleName.psd1" + Show-FileContent -Path $outputManifestPath + } +} diff --git a/.github/actions/Build/scripts/helpers/Build/Add-ContentFromItem.ps1 b/.github/actions/Build/scripts/helpers/Build/Add-ContentFromItem.ps1 new file mode 100644 index 00000000..f8f2fa77 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Add-ContentFromItem.ps1 @@ -0,0 +1,66 @@ +function Add-ContentFromItem { + <# + .SYNOPSIS + Add the content of a folder or file to the root module file. + + .DESCRIPTION + This function will add the content of a folder or file to the root module file. + + .EXAMPLE + Add-ContentFromItem -Path 'C:\MyModule\src\MyModule' -RootModuleFilePath 'C:\MyModule\src\MyModule.psm1' -RootPath 'C:\MyModule\src' + #> + param( + # The path to the folder or file to process. + [Parameter(Mandatory)] + [string] $Path, + + # The path to the root module file. + [Parameter(Mandatory)] + [string] $RootModuleFilePath, + + # The root path of the module. + [Parameter(Mandatory)] + [string] $RootPath + ) + # Get the path separator for the current OS + $pathSeparator = [System.IO.Path]::DirectorySeparatorChar + + $relativeFolderPath = $Path -Replace $RootPath, '' + $relativeFolderPath = $relativeFolderPath -Replace $file.Extension, '' + $relativeFolderPath = $relativeFolderPath.TrimStart($pathSeparator) + $relativeFolderPath = $relativeFolderPath -Split $pathSeparator | ForEach-Object { "[$_]" } + $relativeFolderPath = $relativeFolderPath -Join ' - ' + + Add-Content -Path $RootModuleFilePath -Force -Value @" +#region $relativeFolderPath +Write-Debug "[`$scriptName] - $relativeFolderPath - Processing folder" +"@ + + $files = $Path | Get-ChildItem -File -Force -Filter '*.ps1' | Sort-Object -Property FullName + foreach ($file in $files) { + $relativeFilePath = $file.FullName -Replace $RootPath, '' + $relativeFilePath = $relativeFilePath -Replace $file.Extension, '' + $relativeFilePath = $relativeFilePath.TrimStart($pathSeparator) + $relativeFilePath = $relativeFilePath -Split $pathSeparator | ForEach-Object { "[$_]" } + $relativeFilePath = $relativeFilePath -Join ' - ' + + Add-Content -Path $RootModuleFilePath -Force -Value @" +#region $relativeFilePath +Write-Debug "[`$scriptName] - $relativeFilePath - Importing" +"@ + Get-Content -Path $file.FullName | Add-Content -Path $RootModuleFilePath -Force + Add-Content -Path $RootModuleFilePath -Value @" +Write-Debug "[`$scriptName] - $relativeFilePath - Done" +#endregion $relativeFilePath +"@ + } + + $subFolders = $Path | Get-ChildItem -Directory -Force | Sort-Object -Property Name + foreach ($subFolder in $subFolders) { + Add-ContentFromItem -Path $subFolder.FullName -RootModuleFilePath $RootModuleFilePath -RootPath $RootPath + } + Add-Content -Path $RootModuleFilePath -Force -Value @" +Write-Debug "[`$scriptName] - $relativeFolderPath - Done" +#endregion $relativeFolderPath +"@ +} diff --git a/.github/actions/Build/scripts/helpers/Build/Build-PSModuleBase.ps1 b/.github/actions/Build/scripts/helpers/Build/Build-PSModuleBase.ps1 new file mode 100644 index 00000000..d8c49a87 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Build-PSModuleBase.ps1 @@ -0,0 +1,43 @@ +#Requires -Modules @{ ModuleName = 'GitHub'; ModuleVersion = '0.13.2' } + +function Build-PSModuleBase { + <# + .SYNOPSIS + Compiles the base module files. + + .DESCRIPTION + This function will compile the base module files. + It will copy the source files to the output folder and remove the files that are not needed. + + .EXAMPLE + Build-PSModuleBase -SourceFolderPath 'C:\MyModule\src\MyModule' -OutputFolderPath 'C:\MyModule\build\MyModule' + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', Scope = 'Function', + Justification = 'LogGroup - Scoping affects the variables line of sight.' + )] + param( + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName, + + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleSourceFolder, + + # Path to the folder where the built modules are outputted. + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleOutputFolder + ) + + LogGroup 'Build base' { + Write-Host "Copying files from [$ModuleSourceFolder] to [$ModuleOutputFolder]" + Copy-Item -Path "$ModuleSourceFolder\*" -Destination $ModuleOutputFolder -Recurse -Force -Verbose -Exclude "$ModuleName.psm1" + New-Item -Path $ModuleOutputFolder -Name "$ModuleName.psm1" -ItemType File -Force -Verbose + } + + LogGroup 'Build base - Result' { + (Get-ChildItem -Path $ModuleOutputFolder -Recurse -Force).FullName | Sort-Object + } +} diff --git a/.github/actions/Build/scripts/helpers/Build/Build-PSModuleDocumentation.ps1 b/.github/actions/Build/scripts/helpers/Build/Build-PSModuleDocumentation.ps1 new file mode 100644 index 00000000..b390ebe3 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Build-PSModuleDocumentation.ps1 @@ -0,0 +1,110 @@ +#Requires -Modules @{ ModuleName = 'GitHub'; ModuleVersion = '0.13.2' } +#Requires -Modules @{ ModuleName = 'platyPS'; ModuleVersion = '0.14.2' } +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Build-PSModuleDocumentation { + <# + .SYNOPSIS + Compiles the module documentation. + + .DESCRIPTION + This function will compile the module documentation. + It will generate the markdown files for the module help and copy them to the output folder. + + .EXAMPLE + Build-PSModuleDocumentation -ModuleOutputFolder 'C:\MyModule\src\MyModule' -DocsOutputFolder 'C:\MyModule\build\MyModule' + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', Scope = 'Function', + Justification = 'LogGroup - Scoping affects the variables line of sight.' + )] + param( + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName, + + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleSourceFolder, + + # Folder where the documentation for the modules should be outputted. 'outputs/docs/MyModule' + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $DocsOutputFolder + ) + + LogGroup 'Build docs - Generate markdown help' { + $ModuleName | Remove-Module -Force + Import-Module -Name $ModuleName -Force -RequiredVersion '999.0.0' + Write-Host ($ModuleName | Get-Module) + $null = New-MarkdownHelp -Module $ModuleName -OutputFolder $DocsOutputFolder -Force -Verbose + } + + LogGroup 'Build docs - Fix markdown code blocks' { + Get-ChildItem -Path $DocsOutputFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $content = Get-Content -Path $_.FullName + $fixedOpening = $false + $newContent = @() + foreach ($line in $content) { + if ($line -match '^```$' -and -not $fixedOpening) { + $line = $line -replace '^```$', '```powershell' + $fixedOpening = $true + } elseif ($line -match '^```.+$') { + $fixedOpening = $true + } elseif ($line -match '^```$') { + $fixedOpening = $false + } + $newContent += $line + } + $newContent | Set-Content -Path $_.FullName + } + } + + LogGroup 'Build docs - Fix markdown escape characters' { + Get-ChildItem -Path $DocsOutputFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $content = Get-Content -Path $_.FullName -Raw + $content = $content -replace '\\`', '`' + $content = $content -replace '\\\[', '[' + $content = $content -replace '\\\]', ']' + $content = $content -replace '\\\<', '<' + $content = $content -replace '\\\>', '>' + $content = $content -replace '\\\\', '\' + $content | Set-Content -Path $_.FullName + } + } + + LogGroup 'Build docs - Structure markdown files to match source files' { + $PublicFunctionsFolder = Join-Path $ModuleSourceFolder.FullName 'functions\public' | Get-Item + Get-ChildItem -Path $DocsOutputFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $file = $_ + Write-Host "Processing: $file" + + # find the source code file that matches the markdown file + $scriptPath = Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force | Where-Object { $_.Name -eq ($file.BaseName + '.ps1') } + Write-Host "Found script path: $scriptPath" + $docsFilePath = ($scriptPath.FullName).Replace($PublicFunctionsFolder.FullName, $DocsOutputFolder.FullName).Replace('.ps1', '.md') + Write-Host "Doc file path: $docsFilePath" + $docsFolderPath = Split-Path -Path $docsFilePath -Parent + New-Item -Path $docsFolderPath -ItemType Directory -Force + Move-Item -Path $file.FullName -Destination $docsFilePath -Force + } + # Get the MD files that are in the public functions folder and move them to the same place in the docs folder + Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $file = $_ + Write-Host "Processing: $file" + $docsFilePath = ($file.FullName).Replace($PublicFunctionsFolder.FullName, $DocsOutputFolder.FullName) + Write-Host "Doc file path: $docsFilePath" + $docsFolderPath = Split-Path -Path $docsFilePath -Parent + New-Item -Path $docsFolderPath -ItemType Directory -Force + Move-Item -Path $file.FullName -Destination $docsFilePath -Force + } + } + + Get-ChildItem -Path $DocsOutputFolder -Recurse -Force -Include '*.md' | ForEach-Object { + $fileName = $_.Name + $hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash + LogGroup " - [$fileName] - [$hash]" { + Show-FileContent -Path $_ + } + } +} diff --git a/.github/actions/Build/scripts/helpers/Build/Build-PSModuleManifest.ps1 b/.github/actions/Build/scripts/helpers/Build/Build-PSModuleManifest.ps1 new file mode 100644 index 00000000..002232ad --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Build-PSModuleManifest.ps1 @@ -0,0 +1,462 @@ +#Requires -Modules @{ ModuleName = 'GitHub'; ModuleVersion = '0.13.2' } +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Build-PSModuleManifest { + <# + .SYNOPSIS + Compiles the module manifest. + + .DESCRIPTION + This function will compile the module manifest. + It will generate the module manifest file and copy it to the output folder. + + .EXAMPLE + Build-PSModuleManifest -SourceFolderPath 'C:\MyModule\src\MyModule' -OutputFolderPath 'C:\MyModule\build\MyModule' + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidLongLines', '', Scope = 'Function', + Justification = 'Easier to read the multi ternery operators in a single line.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', Scope = 'Function', + Justification = 'LogGroup - Scoping affects the variables line of sight.' + )] + param( + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName, + + # Folder where the built modules are outputted. 'outputs/modules/MyModule' + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleOutputFolder + ) + + LogGroup 'Build manifest file' { + $sourceManifestFilePath = Join-Path -Path $ModuleOutputFolder -ChildPath "$ModuleName.psd1" + Write-Host "[SourceManifestFilePath] - [$sourceManifestFilePath]" + if (-not (Test-Path -Path $sourceManifestFilePath)) { + Write-Host "[SourceManifestFilePath] - [$sourceManifestFilePath] - Not found" + $sourceManifestFilePath = Join-Path -Path $ModuleOutputFolder -ChildPath 'manifest.psd1' + } + if (-not (Test-Path -Path $sourceManifestFilePath)) { + Write-Host "[SourceManifestFilePath] - [$sourceManifestFilePath] - Not found" + $manifest = @{} + Write-Host '[Manifest] - Loading empty manifest' + } else { + Write-Host "[SourceManifestFilePath] - [$sourceManifestFilePath] - Found" + $manifest = Get-ModuleManifest -Path $sourceManifestFilePath -Verbose:$false + Write-Host '[Manifest] - Loading from file' + Remove-Item -Path $sourceManifestFilePath -Force -Verbose:$false + } + + $rootModule = "$ModuleName.psm1" + $manifest.RootModule = $rootModule + Write-Host "[RootModule] - [$($manifest.RootModule)]" + + $manifest.ModuleVersion = '999.0.0' + Write-Host "[ModuleVersion] - [$($manifest.ModuleVersion)]" + + $manifest.Author = $manifest.Keys -contains 'Author' ? ($manifest.Author | IsNotNullOrEmpty) ? $manifest.Author : $env:GITHUB_REPOSITORY_OWNER : $env:GITHUB_REPOSITORY_OWNER + Write-Host "[Author] - [$($manifest.Author)]" + + $manifest.CompanyName = $manifest.Keys -contains 'CompanyName' ? ($manifest.CompanyName | IsNotNullOrEmpty) ? $manifest.CompanyName : $env:GITHUB_REPOSITORY_OWNER : $env:GITHUB_REPOSITORY_OWNER + Write-Host "[CompanyName] - [$($manifest.CompanyName)]" + + $year = Get-Date -Format 'yyyy' + $copyrightOwner = $manifest.CompanyName -eq $manifest.Author ? $manifest.Author : "$($manifest.Author) | $($manifest.CompanyName)" + $copyright = "(c) $year $copyrightOwner. All rights reserved." + $manifest.Copyright = $manifest.Keys -contains 'Copyright' ? -not [string]::IsNullOrEmpty($manifest.Copyright) ? $manifest.Copyright : $copyright : $copyright + Write-Host "[Copyright] - [$($manifest.Copyright)]" + + $repoDescription = gh repo view --json description | ConvertFrom-Json | Select-Object -ExpandProperty description + $manifest.Description = $manifest.Keys -contains 'Description' ? ($manifest.Description | IsNotNullOrEmpty) ? $manifest.Description : $repoDescription : $repoDescription + Write-Host "[Description] - [$($manifest.Description)]" + + $manifest.PowerShellHostName = $manifest.Keys -contains 'PowerShellHostName' ? -not [string]::IsNullOrEmpty($manifest.PowerShellHostName) ? $manifest.PowerShellHostName : $null : $null + Write-Host "[PowerShellHostName] - [$($manifest.PowerShellHostName)]" + + $manifest.PowerShellHostVersion = $manifest.Keys -contains 'PowerShellHostVersion' ? -not [string]::IsNullOrEmpty($manifest.PowerShellHostVersion) ? $manifest.PowerShellHostVersion : $null : $null + Write-Host "[PowerShellHostVersion] - [$($manifest.PowerShellHostVersion)]" + + $manifest.DotNetFrameworkVersion = $manifest.Keys -contains 'DotNetFrameworkVersion' ? -not [string]::IsNullOrEmpty($manifest.DotNetFrameworkVersion) ? $manifest.DotNetFrameworkVersion : $null : $null + Write-Host "[DotNetFrameworkVersion] - [$($manifest.DotNetFrameworkVersion)]" + + $manifest.ClrVersion = $manifest.Keys -contains 'ClrVersion' ? -not [string]::IsNullOrEmpty($manifest.ClrVersion) ? $manifest.ClrVersion : $null : $null + Write-Host "[ClrVersion] - [$($manifest.ClrVersion)]" + + $manifest.ProcessorArchitecture = $manifest.Keys -contains 'ProcessorArchitecture' ? -not [string]::IsNullOrEmpty($manifest.ProcessorArchitecture) ? $manifest.ProcessorArchitecture : 'None' : 'None' + Write-Host "[ProcessorArchitecture] - [$($manifest.ProcessorArchitecture)]" + + # Get the path separator for the current OS + $pathSeparator = [System.IO.Path]::DirectorySeparatorChar + + Write-Host '[FileList]' + $files = [System.Collections.Generic.List[System.IO.FileInfo]]::new() + + # Get files on module root + $ModuleOutputFolder | Get-ChildItem -File -ErrorAction SilentlyContinue | Where-Object -Property Name -NotLike '*.ps1' | + ForEach-Object { $files.Add($_) } + + # Get files on module subfolders, excluding the following folders 'init', 'classes', 'public', 'private' + $skipList = @('init', 'classes', 'functions', 'variables') + $ModuleOutputFolder | Get-ChildItem -Directory | Where-Object { $_.Name -NotIn $skipList } | + Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue | ForEach-Object { $files.Add($_) } + + # Get the relative file path and store it in the manifest + $files = $files | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.FileList = $files.count -eq 0 ? @() : @($files) + $manifest.FileList | ForEach-Object { Write-Host "[FileList] - [$_]" } + + $requiredAssembliesFolderPath = Join-Path $ModuleOutputFolder 'assemblies' + $nestedModulesFolderPath = Join-Path $ModuleOutputFolder 'modules' + + Write-Host '[RequiredAssemblies]' + $existingRequiredAssemblies = $manifest.RequiredAssemblies + $requiredAssemblies = Get-ChildItem -Path $requiredAssembliesFolderPath -Recurse -File -ErrorAction SilentlyContinue -Filter '*.dll' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart([System.IO.Path]::DirectorySeparatorChar) } + $requiredAssemblies += Get-ChildItem -Path $nestedModulesFolderPath -Recurse -Depth 1 -File -ErrorAction SilentlyContinue -Filter '*.dll' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart([System.IO.Path]::DirectorySeparatorChar) } + $manifest.RequiredAssemblies = if ($existingRequiredAssemblies) { $existingRequiredAssemblies } elseif ($requiredAssemblies.Count -gt 0) { @($requiredAssemblies) } else { @() } + $manifest.RequiredAssemblies | ForEach-Object { Write-Host "[RequiredAssemblies] - [$_]" } + + Write-Host '[NestedModules]' + $existingNestedModules = $manifest.NestedModules + $nestedModules = Get-ChildItem -Path $nestedModulesFolderPath -Recurse -Depth 1 -File -ErrorAction SilentlyContinue -Include '*.psm1', '*.ps1', '*.dll' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart([System.IO.Path]::DirectorySeparatorChar) } + $manifest.NestedModules = if ($existingNestedModules) { $existingNestedModules } elseif ($nestedModules.Count -gt 0) { @($nestedModules) } else { @() } + $manifest.NestedModules | ForEach-Object { Write-Host "[NestedModules] - [$_]" } + + Write-Host '[ScriptsToProcess]' + $existingScriptsToProcess = $manifest.ScriptsToProcess + $allScriptsToProcess = @('scripts') | ForEach-Object { + Write-Host "[ScriptsToProcess] - Processing [$_]" + $scriptsFolderPath = Join-Path $ModuleOutputFolder $_ + Get-ChildItem -Path $scriptsFolderPath -Recurse -File -ErrorAction SilentlyContinue -Include '*.ps1' | Select-Object -ExpandProperty FullName | ForEach-Object { + $_.Replace($ModuleOutputFolder, '').TrimStart([System.IO.Path]::DirectorySeparatorChar) } + } + $manifest.ScriptsToProcess = if ($existingScriptsToProcess) { $existingScriptsToProcess } elseif ($allScriptsToProcess.Count -gt 0) { @($allScriptsToProcess) } else { @() } + $manifest.ScriptsToProcess | ForEach-Object { Write-Host "[ScriptsToProcess] - [$_]" } + + Write-Host '[TypesToProcess]' + $typesToProcess = Get-ChildItem -Path $ModuleOutputFolder -Recurse -File -ErrorAction SilentlyContinue -Include '*.Types.ps1xml' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.TypesToProcess = $typesToProcess.count -eq 0 ? @() : @($typesToProcess) + $manifest.TypesToProcess | ForEach-Object { Write-Host "[TypesToProcess] - [$_]" } + + Write-Host '[FormatsToProcess]' + $formatsToProcess = Get-ChildItem -Path $ModuleOutputFolder -Recurse -File -ErrorAction SilentlyContinue -Include '*.Format.ps1xml' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.FormatsToProcess = $formatsToProcess.count -eq 0 ? @() : @($formatsToProcess) + $manifest.FormatsToProcess | ForEach-Object { Write-Host "[FormatsToProcess] - [$_]" } + + Write-Host '[DscResourcesToExport]' + $dscResourcesToExportFolderPath = Join-Path $ModuleOutputFolder 'resources' + $dscResourcesToExport = Get-ChildItem -Path $dscResourcesToExportFolderPath -Recurse -File -ErrorAction SilentlyContinue -Include '*.psm1' | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.DscResourcesToExport = $dscResourcesToExport.count -eq 0 ? @() : @($dscResourcesToExport) + $manifest.DscResourcesToExport | ForEach-Object { Write-Host "[DscResourcesToExport] - [$_]" } + + $manifest.FunctionsToExport = Get-PSModuleFunctionsToExport -SourceFolderPath $ModuleOutputFolder + $manifest.CmdletsToExport = Get-PSModuleCmdletsToExport -SourceFolderPath $ModuleOutputFolder + $manifest.AliasesToExport = Get-PSModuleAliasesToExport -SourceFolderPath $ModuleOutputFolder + $manifest.VariablesToExport = Get-PSModuleVariablesToExport -SourceFolderPath $ModuleOutputFolder + + Write-Host '[ModuleList]' + $moduleList = Get-ChildItem -Path $ModuleOutputFolder -Recurse -File -ErrorAction SilentlyContinue -Include '*.psm1' | Where-Object -Property Name -NE $rootModule | + Select-Object -ExpandProperty FullName | + ForEach-Object { $_.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) } + $manifest.ModuleList = $moduleList.count -eq 0 ? @() : @($moduleList) + $manifest.ModuleList | ForEach-Object { Write-Host "[ModuleList] - [$_]" } + + Write-Host '[Gather]' + $capturedModules = [System.Collections.Generic.List[System.Object]]::new() + $capturedVersions = [System.Collections.Generic.List[string]]::new() + $capturedPSEdition = [System.Collections.Generic.List[string]]::new() + + $files = $ModuleOutputFolder | Get-ChildItem -Recurse -File -ErrorAction SilentlyContinue + Write-Host "[Gather] - Processing [$($files.Count)] files" + foreach ($file in $files) { + $relativePath = $file.FullName.Replace($ModuleOutputFolder, '').TrimStart($pathSeparator) + Write-Host "[Gather] - [$relativePath]" + + if ($file.extension -in '.psm1', '.ps1') { + $fileContent = Get-Content -Path $file + + switch -Regex ($fileContent) { + # RequiredModules -> REQUIRES -Modules | , @() if not provided + '^\s*#Requires -Modules (.+)$' { + # Add captured module name to array + $capturedMatches = $matches[1].Split(',').trim() + $capturedMatches | ForEach-Object { + $hashtable = '@\{[^}]*\}' + if ($_ -match $hashtable) { + Write-Host " - [#Requires -Modules] - [$_] - Hashtable" + } else { + Write-Host " - [#Requires -Modules] - [$_] - String" + } + $capturedModules.Add($_) + } + } + # PowerShellVersion -> REQUIRES -Version [.], $null if not provided + '^\s*#Requires -Version (.+)$' { + Write-Host " - [#Requires -Version] - [$($matches[1])]" + $capturedVersions.Add($matches[1]) + } + #CompatiblePSEditions -> REQUIRES -PSEdition , $null if not provided + '^\s*#Requires -PSEdition (.+)$' { + Write-Host " - [#Requires -PSEdition] - [$($matches[1])]" + $capturedPSEdition.Add($matches[1]) + } + } + } + } + + <# + $test = [Microsoft.PowerShell.Commands.ModuleSpecification]::new() + [Microsoft.PowerShell.Commands.ModuleSpecification]::TryParse("@{ModuleName = 'Az'; RequiredVersion = '5.0.0' }", [ref]$test) + $test + + $test.ToString() + + $required = [Microsoft.PowerShell.Commands.ModuleSpecification]::new(@{ModuleName = 'Az'; RequiredVersion = '5.0.0' }) + $required.ToString() + #> + + Write-Host '[RequiredModules] - Gathered' + # Group the module specifications by ModuleName + $capturedModules = $capturedModules | ForEach-Object { + $test = [Microsoft.PowerShell.Commands.ModuleSpecification]::new() + if ([Microsoft.PowerShell.Commands.ModuleSpecification]::TryParse($_, [ref]$test)) { + $test + } else { + [Microsoft.PowerShell.Commands.ModuleSpecification]::new($_) + } + } + + $groupedModules = $capturedModules | Group-Object -Property Name + + # Initialize a list to store unique module specifications + $uniqueModules = [System.Collections.Generic.List[System.Object]]::new() + + # Iterate through each group + foreach ($group in $groupedModules) { + $requiredModuleName = $group.Name + Write-Host "Processing required module [$requiredModuleName]" + $requiredVersion = $group.Group.RequiredVersion | ForEach-Object { [Version]$_ } | Sort-Object -Unique + $minimumVersion = $group.Group.Version | ForEach-Object { [Version]$_ } | Sort-Object -Unique | Select-Object -Last 1 + $maximumVersion = $group.Group.MaximumVersion | ForEach-Object { [Version]$_ } | Sort-Object -Unique | Select-Object -First 1 + Write-Host "RequiredVersion: [$($requiredVersion -join ', ')]" + Write-Host "ModuleVersion: [$minimumVersion]" + Write-Host "MaximumVersion: [$maximumVersion]" + + if ($requiredVersion.Count -gt 1) { + throw 'Multiple RequiredVersions specified.' + } + + if (-not $minimumVersion) { + $minimumVersion = [Version]'0.0.0' + } + + if (-not $maximumVersion) { + $maximumVersion = [Version]'9999.9999.9999' + } + + if ($requiredVersion -and ($minimumVersion -gt $requiredVersion)) { + throw 'ModuleVersion is higher than RequiredVersion.' + } + + if ($minimumVersion -gt $maximumVersion) { + throw 'ModuleVersion is higher than MaximumVersion.' + } + if ($requiredVersion -and ($requiredVersion -gt $maximumVersion)) { + throw 'RequiredVersion is higher than MaximumVersion.' + } + + if ($requiredVersion) { + Write-Host '[RequiredModules] - RequiredVersion' + $uniqueModule = @{ + ModuleName = $requiredModuleName + RequiredVersion = $requiredVersion + } + } elseif (($minimumVersion -ne [Version]'0.0.0') -or ($maximumVersion -ne [Version]'9999.9999.9999')) { + Write-Host '[RequiredModules] - ModuleVersion/MaximumVersion' + $uniqueModule = @{ + ModuleName = $requiredModuleName + } + if ($minimumVersion -ne [Version]'0.0.0') { + $uniqueModule['ModuleVersion'] = $minimumVersion + } + if ($maximumVersion -ne [Version]'9999.9999.9999') { + $uniqueModule['MaximumVersion'] = $maximumVersion + } + } else { + Write-Host '[RequiredModules] - Simple string' + $uniqueModule = $requiredModuleName + } + $uniqueModules.Add([Microsoft.PowerShell.Commands.ModuleSpecification]::new($uniqueModule)) + } + + Write-Host '[RequiredModules] - Result' + $manifest.RequiredModules = $uniqueModules + $manifest.RequiredModules | ForEach-Object { Write-Host " - [$($_ | Out-String)]" } + + Write-Host '[PowerShellVersion]' + $capturedVersions = $capturedVersions | Sort-Object -Unique -Descending + $capturedVersions | ForEach-Object { Write-Host "[PowerShellVersion] - [$_]" } + $manifest.PowerShellVersion = $capturedVersions.count -eq 0 ? [version]'5.1' : [version]($capturedVersions | Select-Object -First 1) + Write-Host '[PowerShellVersion] - Selecting version' + Write-Host "[PowerShellVersion] - [$($manifest.PowerShellVersion)]" + + Write-Host '[CompatiblePSEditions]' + $capturedPSEdition = $capturedPSEdition | Sort-Object -Unique + if ($capturedPSEdition.count -eq 2) { + throw "Conflict detected: The module requires both 'Desktop' and 'Core' editions." + + "'Desktop' and 'Core' editions cannot be required at the same time." + } + if ($capturedPSEdition.count -eq 0 -and $manifest.PowerShellVersion -gt '5.1') { + Write-Host "[CompatiblePSEditions] - Defaulting to 'Core', as no PSEdition was specified and PowerShellVersion > 5.1" + $capturedPSEdition = @('Core') + } + $manifest.CompatiblePSEditions = $capturedPSEdition.count -eq 0 ? @('Core', 'Desktop') : @($capturedPSEdition) + $manifest.CompatiblePSEditions | ForEach-Object { Write-Host "[CompatiblePSEditions] - [$_]" } + + if ($manifest.PowerShellVersion -gt '5.1' -and $manifest.CompatiblePSEditions -contains 'Desktop') { + throw "Conflict detected: The module requires PowerShellVersion > 5.1 while CompatiblePSEditions = 'Desktop'" + + "'Desktop' edition is not supported for PowerShellVersion > 5.1" + } + + Write-Host '[PrivateData]' + $privateData = $manifest.Keys -contains 'PrivateData' ? $null -ne $manifest.PrivateData ? $manifest.PrivateData : @{} : @{} + if ($manifest.Keys -contains 'PrivateData') { + $manifest.Remove('PrivateData') + } + + Write-Host '[HelpInfoURI]' + $manifest.HelpInfoURI = $privateData.Keys -contains 'HelpInfoURI' ? $null -ne $privateData.HelpInfoURI ? $privateData.HelpInfoURI : '' : '' + Write-Host "[HelpInfoURI] - [$($manifest.HelpInfoURI)]" + if ([string]::IsNullOrEmpty($manifest.HelpInfoURI)) { + $manifest.Remove('HelpInfoURI') + } + + Write-Host '[DefaultCommandPrefix]' + $manifest.DefaultCommandPrefix = $privateData.Keys -contains 'DefaultCommandPrefix' ? $null -ne $privateData.DefaultCommandPrefix ? $privateData.DefaultCommandPrefix : '' : '' + Write-Host "[DefaultCommandPrefix] - [$($manifest.DefaultCommandPrefix)]" + + $PSData = $privateData.Keys -contains 'PSData' ? $null -ne $privateData.PSData ? $privateData.PSData : @{} : @{} + + Write-Host '[Tags]' + try { + $repoLabels = gh repo view --json repositoryTopics | ConvertFrom-Json | Select-Object -ExpandProperty repositoryTopics | Select-Object -ExpandProperty name + } catch { + $repoLabels = @() + } + $manifestTags = [System.Collections.Generic.List[string]]::new() + $tags = $PSData.Keys -contains 'Tags' ? ($PSData.Tags).Count -gt 0 ? $PSData.Tags : $repoLabels : $repoLabels + $tags | ForEach-Object { $manifestTags.Add($_) } + # Add tags for compatability mode. https://docs.microsoft.com/en-us/powershell/scripting/developer/module/how-to-write-a-powershell-module-manifest?view=powershell-7.1#compatibility-tags + if ($manifest.CompatiblePSEditions -contains 'Desktop') { + if ($manifestTags -notcontains 'PSEdition_Desktop') { + $manifestTags.Add('PSEdition_Desktop') + } + } + if ($manifest.CompatiblePSEditions -contains 'Core') { + if ($manifestTags -notcontains 'PSEdition_Core') { + $manifestTags.Add('PSEdition_Core') + } + } + $manifestTags | ForEach-Object { Write-Host "[Tags] - [$_]" } + $manifest.Tags = $manifestTags + + if ($PSData.Tags -contains 'PSEdition_Core' -and $manifest.PowerShellVersion -lt '6.0') { + throw "[Tags] - Cannot be PSEdition = 'Core' and PowerShellVersion < 6.0" + } + <# + Windows: Packages that are compatible with the Windows Operating System + Linux: Packages that are compatible with Linux Operating Systems + MacOS: Packages that are compatible with the Mac Operating System + https://learn.microsoft.com/en-us/powershell/gallery/concepts/package-manifest-affecting-ui?view=powershellget-2.x#tag-details + #> + + Write-Host '[LicenseUri]' + $licenseUri = "https://github.com/$env:GITHUB_REPOSITORY_OWNER/$env:GITHUB_REPOSITORY_NAME/blob/main/LICENSE" + $manifest.LicenseUri = $PSData.Keys -contains 'LicenseUri' ? $null -ne $PSData.LicenseUri ? $PSData.LicenseUri : $licenseUri : $licenseUri + Write-Host "[LicenseUri] - [$($manifest.LicenseUri)]" + if ([string]::IsNullOrEmpty($manifest.LicenseUri)) { + $manifest.Remove('LicenseUri') + } + + Write-Host '[ProjectUri]' + $projectUri = gh repo view --json url | ConvertFrom-Json | Select-Object -ExpandProperty url + $manifest.ProjectUri = $PSData.Keys -contains 'ProjectUri' ? $null -ne $PSData.ProjectUri ? $PSData.ProjectUri : $projectUri : $projectUri + Write-Host "[ProjectUri] - [$($manifest.ProjectUri)]" + if ([string]::IsNullOrEmpty($manifest.ProjectUri)) { + $manifest.Remove('ProjectUri') + } + + Write-Host '[IconUri]' + $iconUri = "https://raw.githubusercontent.com/$env:GITHUB_REPOSITORY_OWNER/$env:GITHUB_REPOSITORY_NAME/main/icon/icon.png" + $manifest.IconUri = $PSData.Keys -contains 'IconUri' ? $null -ne $PSData.IconUri ? $PSData.IconUri : $iconUri : $iconUri + Write-Host "[IconUri] - [$($manifest.IconUri)]" + if ([string]::IsNullOrEmpty($manifest.IconUri)) { + $manifest.Remove('IconUri') + } + + Write-Host '[ReleaseNotes]' + $manifest.ReleaseNotes = $PSData.Keys -contains 'ReleaseNotes' ? $null -ne $PSData.ReleaseNotes ? $PSData.ReleaseNotes : '' : '' + Write-Host "[ReleaseNotes] - [$($manifest.ReleaseNotes)]" + if ([string]::IsNullOrEmpty($manifest.ReleaseNotes)) { + $manifest.Remove('ReleaseNotes') + } + + Write-Host '[PreRelease]' + # $manifest.PreRelease = "" + # Is managed by the publish action + + Write-Host '[RequireLicenseAcceptance]' + $manifest.RequireLicenseAcceptance = $PSData.Keys -contains 'RequireLicenseAcceptance' ? $null -ne $PSData.RequireLicenseAcceptance ? $PSData.RequireLicenseAcceptance : $false : $false + Write-Host "[RequireLicenseAcceptance] - [$($manifest.RequireLicenseAcceptance)]" + if ($manifest.RequireLicenseAcceptance -eq $false) { + $manifest.Remove('RequireLicenseAcceptance') + } + + Write-Host '[ExternalModuleDependencies]' + $manifest.ExternalModuleDependencies = $PSData.Keys -contains 'ExternalModuleDependencies' ? $null -ne $PSData.ExternalModuleDependencies ? $PSData.ExternalModuleDependencies : @() : @() + if (($manifest.ExternalModuleDependencies).count -eq 0) { + $manifest.Remove('ExternalModuleDependencies') + } else { + $manifest.ExternalModuleDependencies | ForEach-Object { Write-Host "[ExternalModuleDependencies] - [$_]" } + } + + Write-Host 'Creating new manifest file in outputs folder' + $outputManifestPath = Join-Path -Path $ModuleOutputFolder -ChildPath "$ModuleName.psd1" + Write-Host "OutputManifestPath - [$outputManifestPath]" + New-ModuleManifest -Path $outputManifestPath @manifest + } + + LogGroup 'Build manifest file - Result - Before format' { + Show-FileContent -Path $outputManifestPath + } + + LogGroup 'Build manifest file - Format' { + Set-ModuleManifest -Path $outputManifestPath -Verbose + } + + LogGroup 'Build manifest file - Result - After format' { + Show-FileContent -Path $outputManifestPath + } + + LogGroup 'Build manifest file - Validate - Install module dependencies' { + Resolve-PSModuleDependency -ManifestFilePath $outputManifestPath + } + + LogGroup 'Build manifest file - Validate - Test manifest file' { + Test-ModuleManifest -Path $outputManifestPath + } +} diff --git a/.github/actions/Build/scripts/helpers/Build/Build-PSModuleRootModule.ps1 b/.github/actions/Build/scripts/helpers/Build/Build-PSModuleRootModule.ps1 new file mode 100644 index 00000000..c342b213 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Build-PSModuleRootModule.ps1 @@ -0,0 +1,267 @@ +#Requires -Modules @{ ModuleName = 'GitHub'; ModuleVersion = '0.13.2' } +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Build-PSModuleRootModule { + <# + .SYNOPSIS + Compiles the module root module files. + + .DESCRIPTION + This function will compile the modules root module from source files. + It will copy the source files to the output folder and start compiling the module. + During compilation, the source files are added to the root module file in the following order: + + 1. Module header from header.ps1 file. Usually to suppress code analysis warnings/errors and to add [CmdletBinding()] to the module. + 2. Data loader is added if data files are available. + 3. Combines *.ps1 files from the following folders in alphabetical order from each folder: + 1. init + 2. classes/private + 3. classes/public + 4. functions/private + 5. functions/public + 6. variables/private + 7. variables/public + 8. Any remaining *.ps1 on module root. + 4. Adds a class loader for classes found in the classes/public folder. + 5. Export-ModuleMember by using the functions, cmdlets, variables and aliases found in the source files. + - `Functions` will only contain functions that are from the `functions/public` folder. + - `Cmdlets` will only contain cmdlets that are from the `cmdlets/public` folder. + - `Variables` will only contain variables that are from the `variables/public` folder. + - `Aliases` will only contain aliases that are from the functions from the `functions/public` folder. + + .EXAMPLE + Build-PSModuleRootModule -SourceFolderPath 'C:\MyModule\src\MyModule' -OutputFolderPath 'C:\MyModule\build\MyModule' + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', Scope = 'Function', + Justification = 'LogGroup - Scoping affects the variables line of sight.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingWriteHost', '', Scope = 'Function', + Justification = 'Want to just write to the console, not the pipeline.' + )] + param( + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName, + + # Folder where the built modules are outputted. 'outputs/modules/MyModule' + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleOutputFolder + ) + + # Get the path separator for the current OS + $pathSeparator = [System.IO.Path]::DirectorySeparatorChar + + LogGroup 'Build root module' { + $rootModuleFile = New-Item -Path $ModuleOutputFolder -Name "$ModuleName.psm1" -Force + + #region - Analyze source files + + #region - Export-Classes + $classesFolder = Join-Path -Path $ModuleOutputFolder -ChildPath 'classes/public' + $classExports = '' + if (Test-Path -Path $classesFolder) { + $classes = Get-PSModuleClassesToExport -SourceFolderPath $classesFolder + if ($classes.count -gt 0) { + $classExports += @' +#region Class exporter +# Get the internal TypeAccelerators class to use its static methods. +$TypeAcceleratorsClass = [psobject].Assembly.GetType( + 'System.Management.Automation.TypeAccelerators' +) +# Ensure none of the types would clobber an existing type accelerator. +# If a type accelerator with the same name exists, throw an exception. +$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get +# Define the types to export with type accelerators. +$ExportableEnums = @( + +'@ + $classes | Where-Object Type -EQ 'enum' | ForEach-Object { + $classExports += " [$($_.Name)]`n" + } + + $classExports += @' +) +$ExportableEnums | Foreach-Object { Write-Verbose "Exporting enum '$($_.FullName)'." } +foreach ($Type in $ExportableEnums) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + Write-Verbose "Enum already exists [$($Type.FullName)]. Skipping." + } else { + Write-Verbose "Importing enum '$Type'." + $TypeAcceleratorsClass::Add($Type.FullName, $Type) + } +} +$ExportableClasses = @( + +'@ + $classes | Where-Object Type -EQ 'class' | ForEach-Object { + $classExports += " [$($_.Name)]`n" + } + + $classExports += @' +) +$ExportableClasses | Foreach-Object { Write-Verbose "Exporting class '$($_.FullName)'." } +foreach ($Type in $ExportableClasses) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + Write-Verbose "Class already exists [$($Type.FullName)]. Skipping." + } else { + Write-Verbose "Importing class '$Type'." + $TypeAcceleratorsClass::Add($Type.FullName, $Type) + } +} + +# Remove type accelerators when the module is removed. +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + foreach ($Type in ($ExportableEnums + $ExportableClasses)) { + $TypeAcceleratorsClass::Remove($Type.FullName) + } +}.GetNewClosure() +#endregion Class exporter +'@ + } + } + #endregion - Export-Classes + + $exports = [System.Collections.Specialized.OrderedDictionary]::new() + $exports.Add('Alias', (Get-PSModuleAliasesToExport -SourceFolderPath $ModuleOutputFolder)) + $exports.Add('Cmdlet', (Get-PSModuleCmdletsToExport -SourceFolderPath $ModuleOutputFolder)) + $exports.Add('Function', (Get-PSModuleFunctionsToExport -SourceFolderPath $ModuleOutputFolder)) + $exports.Add('Variable', (Get-PSModuleVariablesToExport -SourceFolderPath $ModuleOutputFolder)) + + Write-Host ($exports | Out-String) + #endregion - Analyze source files + + #region - Module header + $headerFilePath = Join-Path -Path $ModuleOutputFolder -ChildPath 'header.ps1' + if (Test-Path -Path $headerFilePath) { + Get-Content -Path $headerFilePath -Raw | Add-Content -Path $rootModuleFile -Force + $headerFilePath | Remove-Item -Force + } else { + Add-Content -Path $rootModuleFile -Force -Value @' +[CmdletBinding()] +param() +'@ + } + #endregion - Module header + + #region - Module post-header + Add-Content -Path $rootModuleFile -Force -Value @' +$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath) +$script:PSModuleInfo = Test-ModuleManifest -Path "$PSScriptRoot\$baseName.psd1" +$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ } +$scriptName = $script:PSModuleInfo.Name +Write-Debug "[$scriptName] - Importing module" +'@ + #endregion - Module post-header + + #region - Data loader + if (Test-Path -Path (Join-Path -Path $ModuleOutputFolder -ChildPath 'data')) { + + Add-Content -Path $rootModuleFile.FullName -Force -Value @' +#region Data importer +Write-Debug "[$scriptName] - [data] - Processing folder" +$dataFolder = (Join-Path $PSScriptRoot 'data') +Write-Debug "[$scriptName] - [data] - [$dataFolder]" +Get-ChildItem -Path "$dataFolder" -Recurse -Force -Include '*.psd1' -ErrorAction SilentlyContinue | ForEach-Object { + Write-Debug "[$scriptName] - [data] - [$($_.BaseName)] - Importing" + New-Variable -Name $_.BaseName -Value (Import-PowerShellDataFile -Path $_.FullName) -Force + Write-Debug "[$scriptName] - [data] - [$($_.BaseName)] - Done" +} +Write-Debug "[$scriptName] - [data] - Done" +#endregion Data importer +'@ + } + #endregion - Data loader + + #region - Add content from subfolders + $scriptFoldersToProcess = @( + 'init', + 'classes/private', + 'classes/public', + 'functions/private', + 'functions/public', + 'variables/private', + 'variables/public' + ) + + foreach ($scriptFolder in $scriptFoldersToProcess) { + $scriptFolder = Join-Path -Path $ModuleOutputFolder -ChildPath $scriptFolder + if (-not (Test-Path -Path $scriptFolder)) { + continue + } + Add-ContentFromItem -Path $scriptFolder -RootModuleFilePath $rootModuleFile -RootPath $ModuleOutputFolder + Remove-Item -Path $scriptFolder -Force -Recurse + } + #endregion - Add content from subfolders + + #region - Add content from *.ps1 files on module root + $files = $ModuleOutputFolder | Get-ChildItem -File -Force -Filter '*.ps1' | Sort-Object -Property FullName + foreach ($file in $files) { + $relativePath = $file.FullName -Replace $ModuleOutputFolder, '' + $relativePath = $relativePath -Replace $file.Extension, '' + $relativePath = $relativePath.TrimStart($pathSeparator) + $relativePath = $relativePath -Split $pathSeparator | ForEach-Object { "[$_]" } + $relativePath = $relativePath -Join ' - ' + + Add-Content -Path $rootModuleFile -Force -Value @" +#region $relativePath +Write-Debug "[`$scriptName] - $relativePath - Importing" +"@ + Get-Content -Path $file.FullName | Add-Content -Path $rootModuleFile -Force + + Add-Content -Path $rootModuleFile -Force -Value @" +Write-Debug "[`$scriptName] - $relativePath - Done" +#endregion $relativePath +"@ + $file | Remove-Item -Force + } + #endregion - Add content from *.ps1 files on module root + + #region - Export-ModuleMember + Add-Content -Path $rootModuleFile -Force -Value $classExports + + $exportsString = Convert-HashtableToString -Hashtable $exports + + Write-Host ($exportsString | Out-String) + + $params = @{ + Path = $rootModuleFile + Force = $true + Value = @" +#region Member exporter +`$exports = $exportsString +Export-ModuleMember @exports +#endregion Member exporter +"@ + } + Add-Content @params + #endregion - Export-ModuleMember + + } + + LogGroup 'Build root module - Result - Before format' { + Write-Host (Show-FileContent -Path $rootModuleFile) + } + + LogGroup 'Build root module - Format' { + $AllContent = Get-Content -Path $rootModuleFile -Raw + $settings = Join-Path -Path $PSScriptRoot 'PSScriptAnalyzer.Tests.psd1' + Invoke-Formatter -ScriptDefinition $AllContent -Settings $settings | + Out-File -FilePath $rootModuleFile -Encoding utf8BOM -Force + } + + LogGroup 'Build root module - Result - After format' { + Write-Host (Show-FileContent -Path $rootModuleFile) + } + + LogGroup 'Build root module - Validate - Import' { + Add-PSModulePath -Path (Split-Path -Path $ModuleOutputFolder -Parent) + Import-PSModule -Path $ModuleOutputFolder -ModuleName $ModuleName + } + + LogGroup 'Build root module - Validate - File list' { + (Get-ChildItem -Path $ModuleOutputFolder -Recurse -Force).FullName | Sort-Object + } +} diff --git a/.github/actions/Build/scripts/helpers/Build/ConvertTo-Hashtable.ps1 b/.github/actions/Build/scripts/helpers/Build/ConvertTo-Hashtable.ps1 new file mode 100644 index 00000000..33568902 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/ConvertTo-Hashtable.ps1 @@ -0,0 +1,32 @@ +function ConvertTo-Hashtable { + <# + .SYNOPSIS + Converts a string to a hashtable. + + .DESCRIPTION + Converts a string to a hashtable. + + .EXAMPLE + ConvertTo-Hashtable -InputString "@{Key1 = 'Value1'; Key2 = 'Value2'}" + + Key Value + --- ----- + Key1 Value1 + Key2 Value2 + + Converts the string to a hashtable. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingInvokeExpression', '', Scope = 'Function', + Justification = 'Converting a string based hashtable to a hashtable.' + )] + [CmdletBinding()] + param ( + # The string to convert to a hashtable. + [Parameter(Mandatory = $true)] + [string]$InputString + ) + + Invoke-Expression $InputString + +} diff --git a/.github/actions/Build/scripts/helpers/Build/Get-PSModuleAliasesToExport.ps1 b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleAliasesToExport.ps1 new file mode 100644 index 00000000..036ae80d --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleAliasesToExport.ps1 @@ -0,0 +1,36 @@ +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Get-PSModuleAliasesToExport { + <# + .SYNOPSIS + Gets the aliases to export from the module manifest. + + .DESCRIPTION + This function will get the aliases to export from the module manifest. + + .EXAMPLE + Get-PSModuleAliasesToExport -SourceFolderPath 'C:\MyModule\src\MyModule' + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $manifestPropertyName = 'AliasesToExport' + + $moduleName = Split-Path -Path $SourceFolderPath -Leaf + $manifestFileName = "$moduleName.psd1" + $manifestFilePath = Join-Path -Path $SourceFolderPath $manifestFileName + + $manifest = Get-ModuleManifest -Path $manifestFilePath -Verbose:$false + + Write-Host "[$manifestPropertyName]" + $aliasesToExport = (($manifest.AliasesToExport).count -eq 0) -or ($manifest.AliasesToExport | IsNullOrEmpty) ? '*' : $manifest.AliasesToExport + $aliasesToExport | ForEach-Object { + Write-Host "[$manifestPropertyName] - [$_]" + } + + $aliasesToExport +} diff --git a/.github/actions/Build/scripts/helpers/Build/Get-PSModuleClassesToExport.ps1 b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleClassesToExport.ps1 new file mode 100644 index 00000000..b01219a6 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleClassesToExport.ps1 @@ -0,0 +1,40 @@ +function Get-PSModuleClassesToExport { + <# + .SYNOPSIS + Gets the classes to export from the module source code. + + .DESCRIPTION + This function will get the classes to export from the module source code. + + .EXAMPLE + Get-PSModuleClassesToExport -SourceFolderPath 'C:\MyModule\src\MyModule' + + Book + BookList + + This will return the classes to export from the module source code. + + .NOTES + Inspired by [about_Classes | Exporting classes with type accelerators](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.4#exporting-classes-with-type-accelerators) + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] + [CmdletBinding()] + param ( + # The path to the module root folder. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $files = Get-ChildItem -Path $SourceFolderPath -Recurse -Include '*.ps1' | Sort-Object -Property FullName + + foreach ($file in $files) { + $content = Get-Content -Path $file.FullName -Raw + $stringMatches = [Regex]::Matches($content, '(?i)^(class|enum)\s+([^\s{]+)', 'Multiline') + foreach ($match in $stringMatches) { + [pscustomobject]@{ + Type = $match.Groups[1].Value + Name = $match.Groups[2].Value + } + } + } +} diff --git a/.github/actions/Build/scripts/helpers/Build/Get-PSModuleCmdletsToExport.ps1 b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleCmdletsToExport.ps1 new file mode 100644 index 00000000..850c10c2 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleCmdletsToExport.ps1 @@ -0,0 +1,36 @@ +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Get-PSModuleCmdletsToExport { + <# + .SYNOPSIS + Gets the cmdlets to export from the module manifest. + + .DESCRIPTION + This function will get the cmdlets to export from the module manifest. + + .EXAMPLE + Get-PSModuleCmdletsToExport -SourceFolderPath 'C:\MyModule\src\MyModule' + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $manifestPropertyName = 'CmdletsToExport' + + $moduleName = Split-Path -Path $SourceFolderPath -Leaf + $manifestFileName = "$moduleName.psd1" + $manifestFilePath = Join-Path -Path $SourceFolderPath $manifestFileName + + $manifest = Get-ModuleManifest -Path $manifestFilePath -Verbose:$false + + Write-Host "[$manifestPropertyName]" + $cmdletsToExport = (($manifest.CmdletsToExport).count -eq 0) -or ($manifest.CmdletsToExport | IsNullOrEmpty) ? '' : $manifest.CmdletsToExport + $cmdletsToExport | ForEach-Object { + Write-Host "[$manifestPropertyName] - [$_]" + } + + $cmdletsToExport +} diff --git a/.github/actions/Build/scripts/helpers/Build/Get-PSModuleFunctionsToExport.ps1 b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleFunctionsToExport.ps1 new file mode 100644 index 00000000..99f34967 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleFunctionsToExport.ps1 @@ -0,0 +1,44 @@ +function Get-PSModuleFunctionsToExport { + <# + .SYNOPSIS + Gets the functions to export from the module manifest. + + .DESCRIPTION + This function will get the functions to export from the module manifest. + + .EXAMPLE + Get-PSModuleFunctionsToExport -SourceFolderPath 'C:\MyModule\src\MyModule' + #> + [CmdletBinding()] + [OutputType([array])] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $manifestPropertyName = 'FunctionsToExport' + + Write-Host "[$manifestPropertyName]" + Write-Host "[$manifestPropertyName] - Checking path for functions and filters" + + $publicFolderPath = Join-Path -Path $SourceFolderPath -ChildPath 'functions/public' + if (-not (Test-Path -Path $publicFolderPath -PathType Container)) { + Write-Host "[$manifestPropertyName] - [Folder not found] - [$publicFolderPath]" + return $functionsToExport + } + Write-Host "[$manifestPropertyName] - [$publicFolderPath]" + $functionsToExport = [Collections.Generic.List[string]]::new() + $scriptFiles = Get-ChildItem -Path $publicFolderPath -Recurse -File -ErrorAction SilentlyContinue -Include '*.ps1' + Write-Host "[$manifestPropertyName] - [$($scriptFiles.Count)]" + foreach ($file in $scriptFiles) { + $fileContent = Get-Content -Path $file.FullName -Raw + $containsFunction = ($fileContent -match 'function ') -or ($fileContent -match 'filter ') + Write-Host "[$manifestPropertyName] - [$($file.BaseName)] - [$containsFunction]" + if ($containsFunction) { + $functionsToExport.Add($file.BaseName) + } + } + + [array]$functionsToExport +} diff --git a/.github/actions/Build/scripts/helpers/Build/Get-PSModuleVariablesToExport.ps1 b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleVariablesToExport.ps1 new file mode 100644 index 00000000..c842a30d --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Get-PSModuleVariablesToExport.ps1 @@ -0,0 +1,45 @@ +function Get-PSModuleVariablesToExport { + <# + .SYNOPSIS + Gets the variables to export from the module manifest. + + .DESCRIPTION + This function will get the variables to export from the module manifest. + + .EXAMPLE + Get-PSModuleVariablesToExport -SourceFolderPath 'C:\MyModule\src\MyModule' + #> + [OutputType([Collections.Generic.List[string]])] + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $SourceFolderPath + ) + + $manifestPropertyName = 'VariablesToExport' + + Write-Host "[$manifestPropertyName]" + + $variableFolderPath = Join-Path -Path $SourceFolderPath -ChildPath 'variables/public' + if (-not (Test-Path -Path $variableFolderPath -PathType Container)) { + Write-Host "[$manifestPropertyName] - [Folder not found] - [$variableFolderPath]" + return $variablesToExport + } + $scriptFilePaths = Get-ChildItem -Path $variableFolderPath -Recurse -File -Filter *.ps1 | Select-Object -ExpandProperty FullName + + $variablesToExport = [Collections.Generic.List[string]]::new() + $scriptFilePaths | ForEach-Object { + $ast = [System.Management.Automation.Language.Parser]::ParseFile($_, [ref]$null, [ref]$null) + $variables = Get-RootLevelVariable -Ast $ast + $variables | ForEach-Object { + $variablesToExport.Add($_) + } + } + + $variablesToExport | ForEach-Object { + Write-Host "[$manifestPropertyName] - [$_]" + } + + $variablesToExport +} diff --git a/.github/actions/Build/scripts/helpers/Build/Get-RootLevelVariables.ps1 b/.github/actions/Build/scripts/helpers/Build/Get-RootLevelVariables.ps1 new file mode 100644 index 00000000..20daece6 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Get-RootLevelVariables.ps1 @@ -0,0 +1,22 @@ +function Get-RootLevelVariable { + <# + .SYNOPSIS + Get the root-level variables in a ast. + + .EXAMPLE + Get-RootLevelVariable -Ast $ast + #> + [CmdletBinding()] + param ( + # The Abstract Syntax Tree (AST) to analyze + [System.Management.Automation.Language.ScriptBlockAst]$Ast + ) + # Iterate over the top-level statements in the AST + foreach ($statement in $Ast.EndBlock.Statements) { + # Check if the statement is an assignment statement + if ($statement -is [System.Management.Automation.Language.AssignmentStatementAst]) { + # Get the variable name, removing the scope prefix + $statement.Left.VariablePath.UserPath -replace '.*:' + } + } +} diff --git a/.github/actions/Build/scripts/helpers/Build/Import-PSModule.ps1 b/.github/actions/Build/scripts/helpers/Build/Import-PSModule.ps1 new file mode 100644 index 00000000..2797069d --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Import-PSModule.ps1 @@ -0,0 +1,50 @@ +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Import-PSModule { + <# + .SYNOPSIS + Imports a build PS module. + + .DESCRIPTION + Imports a build PS module. + + .EXAMPLE + Import-PSModule -SourceFolderPath $ModuleFolderPath -ModuleName $ModuleName + + Imports a module located at $ModuleFolderPath with the name $ModuleName. + #> + [CmdletBinding()] + param( + # Path to the folder where the module source code is located. + [Parameter(Mandatory)] + [string] $Path, + + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName + ) + + $moduleName = Split-Path -Path $Path -Leaf + $manifestFileName = "$moduleName.psd1" + $manifestFilePath = Join-Path -Path $Path $manifestFileName + $manifestFile = Get-ModuleManifest -Path $manifestFilePath -As FileInfo -Verbose + + Write-Host "Manifest file path: [$($manifestFile.FullName)]" -Verbose + $existingModule = Get-Module -Name $ModuleName -ListAvailable + $existingModule | Remove-Module -Force -Verbose + $existingModule.RequiredModules | ForEach-Object { $_ | Remove-Module -Force -Verbose -ErrorAction SilentlyContinue } + $existingModule.NestedModules | ForEach-Object { $_ | Remove-Module -Force -Verbose -ErrorAction SilentlyContinue } + # Get-InstalledPSResource | Where-Object Name -EQ $ModuleName | Uninstall-PSResource -SkipDependencyCheck -Verbose:$false + Resolve-PSModuleDependency -ManifestFilePath $manifestFile + Import-Module -Name $ModuleName -RequiredVersion '999.0.0' + + Write-Host 'List loaded modules' + $availableModules = Get-Module -ListAvailable -Refresh -Verbose:$false + $availableModules | Select-Object Name, Version, Path | Sort-Object Name | Format-Table -AutoSize + Write-Host 'List commands' + Write-Host (Get-Command -Module $moduleName | Format-Table -AutoSize | Out-String) + + if ($ModuleName -notin $availableModules.Name) { + throw 'Module not found' + } +} diff --git a/.github/actions/Build/scripts/helpers/Build/PSScriptAnalyzer.Tests.psd1 b/.github/actions/Build/scripts/helpers/Build/PSScriptAnalyzer.Tests.psd1 new file mode 100644 index 00000000..bd8fb923 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/PSScriptAnalyzer.Tests.psd1 @@ -0,0 +1,56 @@ +@{ + Rules = @{ + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + PSAvoidLongLines = @{ + Enable = $true + MaximumLineLength = 150 + } + PSAvoidSemicolonsAsLineTerminators = @{ + Enable = $true + } + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $false + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + PSProvideCommentHelp = @{ + Enable = $true + ExportedOnly = $false + BlockComment = $true + VSCodeSnippetCorrection = $false + Placement = 'begin' + } + PSUseConsistentIndentation = @{ + Enable = $true + IndentationSize = 4 + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + Kind = 'space' + } + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $true + CheckSeparator = $true + CheckParameter = $true + IgnoreAssignmentOperatorInsideHashTable = $true + } + } + ExcludeRules = @( + 'PSAvoidUsingCmdletAliases', + 'PSUseToExportFieldsInManifest' + ) +} diff --git a/.github/actions/Build/scripts/helpers/Build/Resolve-PSModuleDependency.ps1 b/.github/actions/Build/scripts/helpers/Build/Resolve-PSModuleDependency.ps1 new file mode 100644 index 00000000..4924d3e7 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Resolve-PSModuleDependency.ps1 @@ -0,0 +1,64 @@ +#Requires -Modules @{ ModuleName = 'Retry'; ModuleVersion = '0.1.3' } + +function Resolve-PSModuleDependency { + <# + .SYNOPSIS + Resolve dependencies for a module based on the manifest file. + + .DESCRIPTION + Resolve dependencies for a module based on the manifest file, following PSModuleInfo structure + + .EXAMPLE + Resolve-PSModuleDependency -Path 'C:\MyModule\MyModule.psd1' + + Installs all modules defined in the manifest file, following PSModuleInfo structure. + + .NOTES + Should later be adapted to support both pre-reqs, and dependencies. + Should later be adapted to take 4 parameters sets: specific version ("requiredVersion" | "GUID"), latest version ModuleVersion, + and latest version within a range MinimumVersion - MaximumVersion. + #> + [Alias('Resolve-PSModuleDependencies')] + [CmdletBinding()] + param( + # The path to the manifest file. + [Parameter(Mandatory)] + [string] $ManifestFilePath + ) + + Write-Host 'Resolving dependencies' + + $manifest = Import-PowerShellDataFile -Path $ManifestFilePath + Write-Host "Reading [$ManifestFilePath]" + Write-Host "Found [$($manifest.RequiredModules.Count)] modules to install" + + foreach ($requiredModule in $manifest.RequiredModules) { + $installParams = @{} + + if ($requiredModule -is [string]) { + $installParams.Name = $requiredModule + } else { + $installParams.Name = $requiredModule.ModuleName + $installParams.MinimumVersion = $requiredModule.ModuleVersion + $installParams.RequiredVersion = $requiredModule.RequiredVersion + $installParams.MaximumVersion = $requiredModule.MaximumVersion + } + $installParams.Force = $true + $installParams.Verbose = $false + + Write-Host "[$($installParams.Name)] - Installing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Retry -Count 5 -Delay 10 { + Install-Module @installParams -AllowPrerelease:$false + } + $VerbosePreference = $VerbosePreferenceOriginal + Write-Host "[$($installParams.Name)] - Importing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Import-Module @installParams + $VerbosePreference = $VerbosePreferenceOriginal + Write-Host "[$($installParams.Name)] - Done" + } + Write-Host 'Resolving dependencies - Done' +} diff --git a/.github/actions/Build/scripts/helpers/Build/Update-PSModuleManifestAliasesToExport.ps1 b/.github/actions/Build/scripts/helpers/Build/Update-PSModuleManifestAliasesToExport.ps1 new file mode 100644 index 00000000..eccabdb5 --- /dev/null +++ b/.github/actions/Build/scripts/helpers/Build/Update-PSModuleManifestAliasesToExport.ps1 @@ -0,0 +1,40 @@ +#Requires -Modules @{ ModuleName = 'GitHub'; ModuleVersion = '0.13.2' } +#Requires -Modules @{ ModuleName = 'Utilities'; ModuleVersion = '0.3.0' } + +function Update-PSModuleManifestAliasesToExport { + <# + .SYNOPSIS + Updates the aliases to export in the module manifest. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Updates a file that is being built.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', Scope = 'Function', + Justification = 'LogGroup - Scoping affects the variables line of sight.' + )] + [CmdletBinding()] + param( + # Name of the module. + [Parameter(Mandatory)] + [string] $ModuleName, + + # Folder where the module is outputted. + [Parameter(Mandatory)] + [System.IO.DirectoryInfo] $ModuleOutputFolder + ) + LogGroup 'Updating aliases to export in module manifest' { + Write-Host "Module name: [$ModuleName]" + Write-Host "Module output folder: [$ModuleOutputFolder]" + $aliases = Get-Command -Module $ModuleName -CommandType Alias + Write-Host "Found aliases: [$($aliases.Count)]" + foreach ($alias in $aliases) { + Write-Host "Alias: [$($alias.Name)]" + } + $outputManifestPath = Join-Path -Path $ModuleOutputFolder -ChildPath "$ModuleName.psd1" + Write-Host "Output manifest path: [$outputManifestPath]" + Write-Host 'Setting module manifest with AliasesToExport' + Set-ModuleManifest -Path $outputManifestPath -AliasesToExport $aliases.Name -Verbose + } +} diff --git a/.github/actions/Build/scripts/main.ps1 b/.github/actions/Build/scripts/main.ps1 new file mode 100644 index 00000000..f1ac4532 --- /dev/null +++ b/.github/actions/Build/scripts/main.ps1 @@ -0,0 +1,53 @@ +#Requires -Modules Utilities + +[CmdletBinding()] +param() + +$path = (Join-Path -Path $PSScriptRoot -ChildPath 'helpers') | Get-Item | Resolve-Path -Relative +LogGroup "Loading helper scripts from [$path]" { + Get-ChildItem -Path $path -Filter '*.ps1' -Recurse | Resolve-Path -Relative | ForEach-Object { + Write-Host "$_" + . $_ + } +} + +LogGroup 'Loading inputs' { + $moduleName = ($env:GITHUB_ACTION_INPUT_Name | IsNullOrEmpty) ? $env:GITHUB_REPOSITORY_NAME : $env:GITHUB_ACTION_INPUT_Name + Write-Host "Module name: [$moduleName]" + + $moduleSourceFolderPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $env:GITHUB_ACTION_INPUT_Path/$moduleName + if (-not (Test-Path -Path $moduleSourceFolderPath)) { + $moduleSourceFolderPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $env:GITHUB_ACTION_INPUT_Path + } + Write-Host "Source module path: [$moduleSourceFolderPath]" + if (-not (Test-Path -Path $moduleSourceFolderPath)) { + throw "Module path [$moduleSourceFolderPath] does not exist." + } + + $modulesOutputFolderPath = Join-Path $env:GITHUB_WORKSPACE $env:GITHUB_ACTION_INPUT_ModulesOutputPath + Write-Host "Modules output path: [$modulesOutputFolderPath]" + $docsOutputFolderPath = Join-Path $env:GITHUB_WORKSPACE $env:GITHUB_ACTION_INPUT_DocsOutputPath + Write-Host "Docs output path: [$docsOutputFolderPath]" +} + +LogGroup 'Build local scripts' { + Write-Host 'Execution order:' + $scripts = Get-ChildItem -Filter '*build.ps1' -Recurse | Sort-Object -Property Name | Resolve-Path -Relative + $scripts | ForEach-Object { + Write-Host " - $_" + } + $scripts | ForEach-Object { + LogGroup "Build local scripts - [$_]" { + . $_ + } + } +} + +$params = @{ + ModuleName = $moduleName + ModuleSourceFolderPath = $moduleSourceFolderPath + ModulesOutputFolderPath = $modulesOutputFolderPath + DocsOutputFolderPath = $docsOutputFolderPath +} + +Build-PSModule @params diff --git a/.github/actions/Document/action.yml b/.github/actions/Document/action.yml new file mode 100644 index 00000000..90768f12 --- /dev/null +++ b/.github/actions/Document/action.yml @@ -0,0 +1,27 @@ +name: '{{ NAME }}' +description: '{{ DESCRIPTION }}' +author: PSModule +branding: + icon: upload-cloud + color: white + +inputs: + working-directory: + description: The working directory where Terraform will be executed + required: false + subject: + description: The subject to greet + required: false + default: World + +runs: + using: composite + steps: + - name: '{{ NAME }}' + uses: PSModule/GitHub-Script@v1 + env: + GITHUB_ACTION_INPUT_subject: ${{ inputs.subject }} + with: + Script: | + # '{{ NAME }}' + ${{ github.action_path }}\scripts\main.ps1 diff --git a/.github/actions/Document/scripts/main.ps1 b/.github/actions/Document/scripts/main.ps1 new file mode 100644 index 00000000..855bf763 --- /dev/null +++ b/.github/actions/Document/scripts/main.ps1 @@ -0,0 +1,40 @@ +#Requires -Modules GitHub + +[CmdletBinding()] +param() + +$path = (Join-Path -Path $PSScriptRoot -ChildPath 'helpers') +LogGroup "Loading helper scripts from [$path]" { + Get-ChildItem -Path $path -Filter '*.ps1' -Recurse | ForEach-Object { + Write-Host "[$($_.FullName)]" + . $_.FullName + } +} + +LogGroup 'Loading inputs' { + $moduleName = ($env:GITHUB_ACTION_INPUT_Name | IsNullOrEmpty) ? $env:GITHUB_REPOSITORY_NAME : $env:GITHUB_ACTION_INPUT_Name + Write-Host "Module name: [$moduleName]" + + $moduleSourceFolderPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $env:GITHUB_ACTION_INPUT_Path $moduleName + if (-not (Test-Path -Path $moduleSourceFolderPath)) { + $moduleSourceFolderPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $env:GITHUB_ACTION_INPUT_Path + } + Write-Host "Source module path: [$moduleSourceFolderPath]" + if (-not (Test-Path -Path $moduleSourceFolderPath)) { + throw "Module path [$moduleSourceFolderPath] does not exist." + } + + $modulesOutputFolderPath = Join-Path $env:GITHUB_WORKSPACE $env:GITHUB_ACTION_INPUT_ModulesOutputPath + Write-Host "Modules output path: [$modulesOutputFolderPath]" + $docsOutputFolderPath = Join-Path $env:GITHUB_WORKSPACE $env:GITHUB_ACTION_INPUT_DocsOutputPath + Write-Host "Docs output path: [$docsOutputFolderPath]" +} + +$params = @{ + ModuleName = $moduleName + ModuleSourceFolderPath = $moduleSourceFolderPath + ModulesOutputFolderPath = $modulesOutputFolderPath + DocsOutputFolderPath = $docsOutputFolderPath +} + +Build-PSModule @params diff --git a/.github/actions/Initialize/action.yml b/.github/actions/Initialize/action.yml new file mode 100644 index 00000000..7441d6f6 --- /dev/null +++ b/.github/actions/Initialize/action.yml @@ -0,0 +1,37 @@ +name: Initialize-PSModule (by PSModule) +description: Prepare runner for the PSModule framework. +author: PSModule +branding: + icon: loader + color: gray-dark + +inputs: + Debug: + description: Enable debug output. + required: false + default: 'false' + Verbose: + description: Enable verbose output. + required: false + default: 'false' + Version: + description: Specifies the version of the GitHub module to be installed. The value must be an exact version. + required: false + Prerelease: + description: Allow prerelease versions if available. + required: false + default: 'false' + +runs: + using: composite + steps: + - name: Initialize-PSModule + uses: PSModule/GitHub-Script@v1 + with: + Debug: ${{ inputs.Debug }} + Prerelease: ${{ inputs.Prerelease }} + Verbose: ${{ inputs.Verbose }} + Version: ${{ inputs.Version }} + Script: | + # Initialize-PSModule + . "${{ github.action_path }}\scripts\main.ps1" diff --git a/.github/actions/Initialize/scripts/main.ps1 b/.github/actions/Initialize/scripts/main.ps1 new file mode 100644 index 00000000..3a496a9b --- /dev/null +++ b/.github/actions/Initialize/scripts/main.ps1 @@ -0,0 +1,46 @@ +#Requires -Modules GitHub + +[CmdletBinding()] +param() + +$requiredModules = @{ + Utilities = @{} + Retry = @{} + 'powershell-yaml' = @{} + PSSemVer = @{} + Pester = @{} + PSScriptAnalyzer = @{} + PlatyPS = @{} + MarkdownPS = @{} + # 'Microsoft.PowerShell.PlatyPS' = @{ + # Prerelease = $true + # } +} + +$requiredModules.GetEnumerator() | Sort-Object | ForEach-Object { + $name = $_.Key + $settings = $_.Value + LogGroup "Installing prerequisite: [$name]" { + $Count = 5 + $Delay = 10 + for ($i = 1; $i -le $Count; $i++) { + try { + Install-PSResource -Name $name -TrustRepository -Repository PSGallery @settings + break + } catch { + if ($i -eq $Count) { + throw $_ + } + Start-Sleep -Seconds $Delay + } + } + Write-Host "Installed module: [$name]" + Write-Host (Get-PSResource -Name $name | Select-Object * | Out-String) + + Write-Host 'Module commands:' + Write-Host (Get-Command -Module $name | Out-String) + } +} + +$requiredModules.Keys | Get-InstalledPSResource -Verbose:$false | Sort-Object -Property Name | + Format-Table -Property Name, Version, Prerelease, Repository -AutoSize -Wrap diff --git a/.github/actions/Publish/action.yml b/.github/actions/Publish/action.yml new file mode 100644 index 00000000..1f4bb6c2 --- /dev/null +++ b/.github/actions/Publish/action.yml @@ -0,0 +1,106 @@ +name: Publish-PSModule (by PSModule) +description: Publish a PowerShell module to the PowerShell Gallery. +author: PSModule +branding: + icon: upload-cloud + color: gray-dark + +inputs: + Name: + description: Name of the module to publish. + required: false + ModulePath: + description: Path to the module to publish. + required: false + default: outputs/modules + APIKey: + description: PowerShell Gallery API Key. + required: true + AutoCleanup: + description: Control wether to automatically delete the prerelease tags after the stable release is created. + required: false + default: 'true' + AutoPatching: + description: Control wether to automatically handle patches. If disabled, the action will only create a patch release if the pull request has a 'patch' label. + required: false + default: 'true' + ConfigurationFile: + description: The path to the configuration file. Settings in the configuration file take precedence over the action inputs. + required: false + default: .github\auto-release.yml + DatePrereleaseFormat: + description: If specified, uses a date based prerelease scheme. The format should be a valid .NET format string like 'yyyyMMddHHmm'. + required: false + default: '' + IgnoreLabels: + description: A comma separated list of labels that do not trigger a release. + required: false + default: NoRelease + IncrementalPrerelease: + description: Control wether to automatically increment the prerelease number. If disabled, the action will ensure only one prerelease exists for a given branch. + required: false + default: 'true' + MajorLabels: + description: A comma separated list of labels that trigger a major release. + required: false + default: major, breaking + MinorLabels: + description: A comma separated list of labels that trigger a minor release. + required: false + default: minor, feature + PatchLabels: + description: A comma separated list of labels that trigger a patch release. + required: false + default: patch, fix + VersionPrefix: + description: The prefix to use for the version number. + required: false + default: v + WhatIf: + description: If specified, the action will only log the changes it would make, but will not actually create or delete any releases or tags. + required: false + default: 'false' + Debug: + description: Enable debug output. + required: false + default: 'false' + Verbose: + description: Enable verbose output. + required: false + default: 'false' + Version: + description: Specifies the version of the GitHub module to be installed. The value must be an exact version. + required: false + Prerelease: + description: Allow prerelease versions if available. + required: false + default: 'false' + +runs: + using: composite + steps: + - name: Run Publish-PSModule + uses: PSModule/GitHub-Script@v1 + env: + GITHUB_ACTION_INPUT_Name: ${{ inputs.Name }} + GITHUB_ACTION_INPUT_ModulePath: ${{ inputs.ModulePath }} + GITHUB_ACTION_INPUT_APIKey: ${{ inputs.APIKey }} + GITHUB_ACTION_INPUT_AutoCleanup: ${{ inputs.AutoCleanup }} + GITHUB_ACTION_INPUT_AutoPatching: ${{ inputs.AutoPatching }} + GITHUB_ACTION_INPUT_ConfigurationFile: ${{ inputs.ConfigurationFile }} + GITHUB_ACTION_INPUT_DatePrereleaseFormat: ${{ inputs.DatePrereleaseFormat }} + GITHUB_ACTION_INPUT_IgnoreLabels: ${{ inputs.IgnoreLabels }} + GITHUB_ACTION_INPUT_IncrementalPrerelease: ${{ inputs.IncrementalPrerelease }} + GITHUB_ACTION_INPUT_MajorLabels: ${{ inputs.MajorLabels }} + GITHUB_ACTION_INPUT_MinorLabels: ${{ inputs.MinorLabels }} + GITHUB_ACTION_INPUT_PatchLabels: ${{ inputs.PatchLabels }} + GITHUB_ACTION_INPUT_VersionPrefix: ${{ inputs.VersionPrefix }} + GITHUB_ACTION_INPUT_WhatIf: ${{ inputs.WhatIf }} + with: + Debug: ${{ inputs.Debug }} + Prerelease: ${{ inputs.Prerelease }} + Verbose: ${{ inputs.Verbose }} + Version: ${{ inputs.Version }} + Script: | + # Publish-PSModule + ${{ github.action_path }}\scripts\main.ps1 diff --git a/.github/actions/Publish/scripts/helpers/Publish-PSModule.ps1 b/.github/actions/Publish/scripts/helpers/Publish-PSModule.ps1 new file mode 100644 index 00000000..c0c1b5f8 --- /dev/null +++ b/.github/actions/Publish/scripts/helpers/Publish-PSModule.ps1 @@ -0,0 +1,415 @@ +#Requires -Modules Utilities, PowerShellGet, Microsoft.PowerShell.PSResourceGet, Retry, GitHub, PSSemVer + +function Publish-PSModule { + <# + .SYNOPSIS + Publishes a module to the PowerShell Gallery and GitHub Pages. + + .DESCRIPTION + Publishes a module to the PowerShell Gallery and GitHub Pages. + + .EXAMPLE + Publish-PSModule -Name 'PSModule.FX' -APIKey $env:PSGALLERY_API_KEY + #> + [OutputType([void])] + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', Scope = 'Function', + Justification = 'LogGroup - Scoping affects the variables line of sight.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', '', + Justification = 'LogGroup - Scoping affects the variables line of sight.' + )] + param( + # Name of the module to process. + [Parameter()] + [string] $Name, + + # The path to the module to process. + [Parameter(Mandatory)] + [string] $ModulePath, + + # The API key for the destination repository. + [Parameter(Mandatory)] + [string] $APIKey + ) + + LogGroup 'Set configuration' { + if (-not (Test-Path -Path $env:GITHUB_ACTION_INPUT_ConfigurationFile -PathType Leaf)) { + Write-Output "Configuration file not found at [$env:GITHUB_ACTION_INPUT_ConfigurationFile]" + } else { + Write-Output "Reading from configuration file [$env:GITHUB_ACTION_INPUT_ConfigurationFile]" + $configuration = ConvertFrom-Yaml -Yaml (Get-Content $env:GITHUB_ACTION_INPUT_ConfigurationFile -Raw) + } + + $autoCleanup = ($configuration.AutoCleanup | IsNotNullOrEmpty) ? $configuration.AutoCleanup -eq 'true' : $env:GITHUB_ACTION_INPUT_AutoCleanup -eq 'true' + $autoPatching = ($configuration.AutoPatching | IsNotNullOrEmpty) ? $configuration.AutoPatching -eq 'true' : $env:GITHUB_ACTION_INPUT_AutoPatching -eq 'true' + $datePrereleaseFormat = ($configuration.DatePrereleaseFormat | IsNotNullOrEmpty) ? $configuration.DatePrereleaseFormat : $env:GITHUB_ACTION_INPUT_DatePrereleaseFormat + $incrementalPrerelease = ($configuration.IncrementalPrerelease | IsNotNullOrEmpty) ? $configuration.IncrementalPrerelease -eq 'true' : $env:GITHUB_ACTION_INPUT_IncrementalPrerelease -eq 'true' + $versionPrefix = ($configuration.VersionPrefix | IsNotNullOrEmpty) ? $configuration.VersionPrefix : $env:GITHUB_ACTION_INPUT_VersionPrefix + $whatIf = ($configuration.WhatIf | IsNotNullOrEmpty) ? $configuration.WhatIf -eq 'true' : $env:GITHUB_ACTION_INPUT_WhatIf -eq 'true' + + $ignoreLabels = (($configuration.IgnoreLabels | IsNotNullOrEmpty) ? $configuration.IgnoreLabels : $env:GITHUB_ACTION_INPUT_IgnoreLabels) -split ',' | ForEach-Object { $_.Trim() } + $majorLabels = (($configuration.MajorLabels | IsNotNullOrEmpty) ? $configuration.MajorLabels : $env:GITHUB_ACTION_INPUT_MajorLabels) -split ',' | ForEach-Object { $_.Trim() } + $minorLabels = (($configuration.MinorLabels | IsNotNullOrEmpty) ? $configuration.MinorLabels : $env:GITHUB_ACTION_INPUT_MinorLabels) -split ',' | ForEach-Object { $_.Trim() } + $patchLabels = (($configuration.PatchLabels | IsNotNullOrEmpty) ? $configuration.PatchLabels : $env:GITHUB_ACTION_INPUT_PatchLabels) -split ',' | ForEach-Object { $_.Trim() } + + Write-Output '-------------------------------------------------' + Write-Output "Auto cleanup enabled: [$autoCleanup]" + Write-Output "Auto patching enabled: [$autoPatching]" + Write-Output "Date-based prerelease format: [$datePrereleaseFormat]" + Write-Output "Incremental prerelease enabled: [$incrementalPrerelease]" + Write-Output "Version prefix: [$versionPrefix]" + Write-Output "What if mode: [$whatIf]" + Write-Output '' + Write-Output "Ignore labels: [$($ignoreLabels -join ', ')]" + Write-Output "Major labels: [$($majorLabels -join ', ')]" + Write-Output "Minor labels: [$($minorLabels -join ', ')]" + Write-Output "Patch labels: [$($patchLabels -join ', ')]" + Write-Output '-------------------------------------------------' + } + + LogGroup 'Event information - JSON' { + $githubEventJson = Get-Content $env:GITHUB_EVENT_PATH + $githubEventJson | Format-List + } + + LogGroup 'Event information - Object' { + $githubEvent = $githubEventJson | ConvertFrom-Json + $pull_request = $githubEvent.pull_request + $githubEvent | Format-List + } + + LogGroup 'Event information - Details' { + $defaultBranchName = (gh repo view --json defaultBranchRef | ConvertFrom-Json | Select-Object -ExpandProperty defaultBranchRef).name + $isPullRequest = $githubEvent.PSObject.Properties.Name -Contains 'pull_request' + if (-not ($isPullRequest -or $whatIf)) { + Write-Warning '⚠️ A release should not be created in this context. Exiting.' + exit + } + $actionType = $githubEvent.action + $isMerged = $pull_request.merged -eq 'True' + $prIsClosed = $pull_request.state -eq 'closed' + $prBaseRef = $pull_request.base.ref + $prHeadRef = $pull_request.head.ref + $targetIsDefaultBranch = $pull_request.base.ref -eq $defaultBranchName + + Write-Output '-------------------------------------------------' + Write-Output "Default branch: [$defaultBranchName]" + Write-Output "Is a pull request event: [$isPullRequest]" + Write-Output "Action type: [$actionType]" + Write-Output "PR Merged: [$isMerged]" + Write-Output "PR Closed: [$prIsClosed]" + Write-Output "PR Base Ref: [$prBaseRef]" + Write-Output "PR Head Ref: [$prHeadRef]" + Write-Output "Target is default branch: [$targetIsDefaultBranch]" + Write-Output '-------------------------------------------------' + } + + LogGroup 'Pull request - details' { + $pull_request | Format-List + } + + LogGroup 'Pull request - Labels' { + $labels = @() + $labels += $pull_request.labels.name + $labels | Format-List + } + + LogGroup 'Calculate release type' { + $createRelease = $isMerged -and $targetIsDefaultBranch + $closedPullRequest = $prIsClosed -and -not $isMerged + $createPrerelease = $labels -Contains 'prerelease' -and -not $createRelease -and -not $closedPullRequest + $prereleaseName = $prHeadRef -replace '[^a-zA-Z0-9]' + + $ignoreRelease = ($labels | Where-Object { $ignoreLabels -contains $_ }).Count -gt 0 + if ($ignoreRelease) { + Write-Output 'Ignoring release creation.' + return + } + + $majorRelease = ($labels | Where-Object { $majorLabels -contains $_ }).Count -gt 0 + $minorRelease = ($labels | Where-Object { $minorLabels -contains $_ }).Count -gt 0 -and -not $majorRelease + $patchRelease = (($labels | Where-Object { $patchLabels -contains $_ }).Count -gt 0 -or $autoPatching) -and -not $majorRelease -and -not $minorRelease + + Write-Output '-------------------------------------------------' + Write-Output "Create a release: [$createRelease]" + Write-Output "Create a prerelease: [$createPrerelease]" + Write-Output "Create a major release: [$majorRelease]" + Write-Output "Create a minor release: [$minorRelease]" + Write-Output "Create a patch release: [$patchRelease]" + Write-Output "Closed pull request: [$closedPullRequest]" + Write-Output '-------------------------------------------------' + } + + LogGroup 'Get latest version - GitHub' { + $releases = gh release list --json 'createdAt,isDraft,isLatest,isPrerelease,name,publishedAt,tagName' | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { + Write-Error 'Failed to list all releases for the repo.' + exit $LASTEXITCODE + } + $releases | Select-Object -Property name, isPrerelease, isLatest, publishedAt | Format-Table + + $latestRelease = $releases | Where-Object { $_.isLatest -eq $true } + $latestRelease | Format-List + $ghReleaseVersionString = $latestRelease.tagName + if ($ghReleaseVersionString | IsNotNullOrEmpty) { + $ghReleaseVersion = New-PSSemVer -Version $ghReleaseVersionString + } else { + Write-Warning 'Could not find the latest release version. Using ''0.0.0'' as the version.' + $ghReleaseVersion = New-PSSemVer -Version '0.0.0' + } + Write-Output '-------------------------------------------------' + Write-Output 'GitHub version:' + Write-Output ($ghReleaseVersion | Format-Table | Out-String) + Write-Output $ghReleaseVersion.ToString() + Write-Output '-------------------------------------------------' + } + + LogGroup 'Get latest version - PSGallery' { + try { + Retry -Count 5 -Delay 10 { + Write-Output "Finding module [$Name] in the PowerShell Gallery." + $latest = Find-PSResource -Name $Name -Repository PSGallery -Verbose:$false + Write-Output ($latest | Format-Table | Out-String) + } -Catch { + throw $_ + } + $psGalleryVersion = New-PSSemVer -Version $latest.Version + } catch { + Write-Warning 'Could not find module online. Using ''0.0.0'' as the version.' + $psGalleryVersion = New-PSSemVer -Version '0.0.0' + } + Write-Output '-------------------------------------------------' + Write-Output 'PSGallery version:' + Write-Output ($psGalleryVersion | Format-Table | Out-String) + Write-Output $psGalleryVersion.ToString() + Write-Output '-------------------------------------------------' + } + + LogGroup 'Get latest version - Manifest' { + Add-PSModulePath -Path (Split-Path -Path $ModulePath -Parent) + $manifestFilePath = Join-Path $ModulePath "$Name.psd1" + Write-Output "Module manifest file path: [$manifestFilePath]" + if (-not (Test-Path -Path $manifestFilePath)) { + Write-Error "Module manifest file not found at [$manifestFilePath]" + return + } + try { + $manifestVersion = New-PSSemVer -Version (Test-ModuleManifest $manifestFilePath -Verbose:$false).Version + } catch { + if ($manifestVersion | IsNullOrEmpty) { + Write-Warning 'Could not find the module version in the manifest. Using ''0.0.0'' as the version.' + $manifestVersion = New-PSSemVer -Version '0.0.0' + } + } + Write-Output '-------------------------------------------------' + Write-Output 'Manifest version:' + Write-Output ($manifestVersion | Format-Table | Out-String) + Write-Output $manifestVersion.ToString() + Write-Output '-------------------------------------------------' + } + + LogGroup 'Get latest version' { + Write-Output "GitHub: [$($ghReleaseVersion.ToString())]" + Write-Output "PSGallery: [$($psGalleryVersion.ToString())]" + Write-Output "Manifest: [$($manifestVersion.ToString())] (ignored)" + $latestVersion = New-PSSemVer -Version ($psGalleryVersion, $ghReleaseVersion | Sort-Object -Descending | Select-Object -First 1) + Write-Output '-------------------------------------------------' + Write-Output 'Latest version:' + Write-Output ($latestVersion | Format-Table | Out-String) + Write-Output $latestVersion.ToString() + Write-Output '-------------------------------------------------' + } + + LogGroup 'Calculate new version' { + # - Increment based on label on PR + $newVersion = New-PSSemVer -Version $latestVersion + $newVersion.Prefix = $versionPrefix + if ($majorRelease) { + Write-Output 'Incrementing major version.' + $newVersion.BumpMajor() + } elseif ($minorRelease) { + Write-Output 'Incrementing minor version.' + $newVersion.BumpMinor() + } elseif ($patchRelease) { + Write-Output 'Incrementing patch version.' + $newVersion.BumpPatch() + } else { + Write-Output 'Skipping release creation, exiting.' + return + } + + Write-Output "Partial new version: [$newVersion]" + + if ($createPrerelease) { + Write-Output "Adding a prerelease tag to the version using the branch name [$prereleaseName]." + Write-Output ($releases | Where-Object { $_.tagName -like "*$prereleaseName*" } | + Select-Object -Property name, isPrerelease, isLatest, publishedAt | Format-Table -AutoSize | Out-String) + + $newVersion.Prerelease = $prereleaseName + Write-Output "Partial new version: [$newVersion]" + + if ($datePrereleaseFormat | IsNotNullOrEmpty) { + Write-Output "Using date-based prerelease: [$datePrereleaseFormat]." + $newVersion.Prerelease += "$(Get-Date -Format $datePrereleaseFormat)" + Write-Output "Partial new version: [$newVersion]" + } + + if ($incrementalPrerelease) { + # Find the latest prerelease version + $newVersionString = "$($newVersion.Major).$($newVersion.Minor).$($newVersion.Patch)" + + # PowerShell Gallery + $params = @{ + Name = $Name + Version = '*' + Prerelease = $true + Repository = 'PSGallery' + Verbose = $false + ErrorAction = 'SilentlyContinue' + } + Write-Output 'Finding the latest prerelease version in the PowerShell Gallery.' + Write-Output ($params | Format-Table | Out-String) + $psGalleryPrereleases = Find-PSResource @params + $psGalleryPrereleases = $psGalleryPrereleases | Where-Object { $_.Version -like "$newVersionString" } + $psGalleryPrereleases = $psGalleryPrereleases | Where-Object { $_.Prerelease -like "$prereleaseName*" } + $latestPSGalleryPrerelease = $psGalleryPrereleases.Prerelease | ForEach-Object { + [int]($_ -replace $prereleaseName) + } | Sort-Object | Select-Object -Last 1 + Write-Output "PSGallery prerelease: [$latestPSGalleryPrerelease]" + + # GitHub + $ghPrereleases = $releases | Where-Object { $_.tagName -like "*$newVersionString*" } + $ghPrereleases = $ghPrereleases | Where-Object { $_.tagName -like "*$prereleaseName*" } + $latestGHPrereleases = $ghPrereleases.tagName | ForEach-Object { + $number = $_ + $number = $number -replace '\.' + $number = ($number -split $prereleaseName, 2)[-1] + [int]$number + } | Sort-Object | Select-Object -Last 1 + Write-Output "GitHub prerelease: [$latestGHPrereleases]" + + $latestPrereleaseNumber = [Math]::Max($latestPSGalleryPrerelease, $latestGHPrereleases) + $latestPrereleaseNumber++ + $latestPrereleaseNumber = ([string]$latestPrereleaseNumber).PadLeft(3, '0') + $newVersion.Prerelease += $latestPrereleaseNumber + } + } + Write-Output '-------------------------------------------------' + Write-Output 'New version:' + Write-Output ($newVersion | Format-Table | Out-String) + Write-Output $newVersion.ToString() + Write-Output '-------------------------------------------------' + } + Write-Output "New version is [$($newVersion.ToString())]" + + LogGroup 'Update module manifest' { + Write-Output 'Bump module version -> module metadata: Update-ModuleMetadata' + $manifestNewVersion = "$($newVersion.Major).$($newVersion.Minor).$($newVersion.Patch)" + Set-ModuleManifest -Path $manifestFilePath -ModuleVersion $manifestNewVersion -Verbose:$false + if ($createPrerelease) { + Write-Output "Prerelease is: [$($newVersion.Prerelease)]" + Set-ModuleManifest -Path $manifestFilePath -Prerelease $($newVersion.Prerelease) -Verbose:$false + } + + Show-FileContent -Path $manifestFilePath + } + + LogGroup 'Install module dependencies' { + Resolve-PSModuleDependency -ManifestFilePath $manifestFilePath + } + + if ($createPrerelease -or $createRelease -or $whatIf) { + LogGroup 'Publish-ToPSGallery' { + if ($createPrerelease) { + $publishPSVersion = "$($newVersion.Major).$($newVersion.Minor).$($newVersion.Patch)-$($newVersion.Prerelease)" + $psGalleryReleaseLink = "https://www.powershellgallery.com/packages/$Name/$publishPSVersion" + } else { + $publishPSVersion = $newVersion.ToString() + $psGalleryReleaseLink = "https://www.powershellgallery.com/packages/$Name/$($newVersion.ToString())" + } + Write-Output "Publish module to PowerShell Gallery using [$APIKey]" + if ($whatIf) { + Write-Output "Publish-PSResource -Path $ModulePath -Repository PSGallery -ApiKey $APIKey" + } else { + try { + Publish-PSResource -Path $ModulePath -Repository PSGallery -ApiKey $APIKey + } catch { + Write-Error $_.Exception.Message + exit $LASTEXITCODE + } + } + if ($whatIf) { + Write-Output "gh pr comment $($pull_request.number) -b 'Published to the PowerShell Gallery [$publishPSVersion]($psGalleryReleaseLink) has been created.'" + } else { + Write-GitHubNotice "Module [$Name - $publishPSVersion] published to the PowerShell Gallery." + gh pr comment $pull_request.number -b "Module [$Name - $publishPSVersion]($psGalleryReleaseLink) published to the PowerShell Gallery." + if ($LASTEXITCODE -ne 0) { + Write-Error 'Failed to comment on the pull request.' + exit $LASTEXITCODE + } + } + } + + LogGroup 'New-GitHubRelease' { + Write-Output 'Create new GitHub release' + if ($createPrerelease) { + if ($whatIf) { + Write-Output "WhatIf: gh release create $newVersion --title $newVersion --target $prHeadRef --generate-notes --prerelease" + } else { + $releaseURL = gh release create $newVersion --title $newVersion --target $prHeadRef --generate-notes --prerelease + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to create the release [$newVersion]." + exit $LASTEXITCODE + } + } + } else { + if ($whatIf) { + Write-Output "WhatIf: gh release create $newVersion --title $newVersion --generate-notes" + } else { + $releaseURL = gh release create $newVersion --title $newVersion --generate-notes + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to create the release [$newVersion]." + exit $LASTEXITCODE + } + } + } + if ($whatIf) { + Write-Output 'WhatIf: gh pr comment $pull_request.number -b "The release [$newVersion] has been created."' + } else { + gh pr comment $pull_request.number -b "GitHub release for $Name [$newVersion]($releaseURL) has been created." + if ($LASTEXITCODE -ne 0) { + Write-Error 'Failed to comment on the pull request.' + exit $LASTEXITCODE + } + } + Write-GitHubNotice "Release created: [$newVersion]" + } + } + + LogGroup 'List prereleases using the same name' { + $prereleasesToCleanup = $releases | Where-Object { $_.tagName -like "*$prereleaseName*" } + $prereleasesToCleanup | Select-Object -Property name, publishedAt, isPrerelease, isLatest | Format-Table + } + + if ((($closedPullRequest -or $createRelease) -and $autoCleanup) -or $whatIf) { + LogGroup "Cleanup prereleases for [$prereleaseName]" { + foreach ($rel in $prereleasesToCleanup) { + $relTagName = $rel.tagName + Write-Output "Deleting prerelease: [$relTagName]." + if ($whatIf) { + Write-Output "WhatIf: gh release delete $($rel.tagName) --cleanup-tag --yes" + } else { + gh release delete $rel.tagName --cleanup-tag --yes + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to delete release [$relTagName]." + exit $LASTEXITCODE + } + } + } + } + } + +} diff --git a/.github/actions/Publish/scripts/helpers/Resolve-PSModuleDependency.ps1 b/.github/actions/Publish/scripts/helpers/Resolve-PSModuleDependency.ps1 new file mode 100644 index 00000000..7d8d0675 --- /dev/null +++ b/.github/actions/Publish/scripts/helpers/Resolve-PSModuleDependency.ps1 @@ -0,0 +1,64 @@ +#Requires -Modules Retry + +function Resolve-PSModuleDependency { + <# + .SYNOPSIS + Resolve dependencies for a module based on the manifest file. + + .DESCRIPTION + Resolve dependencies for a module based on the manifest file, following PSModuleInfo structure + + .EXAMPLE + Resolve-PSModuleDependency -Path 'C:\MyModule\MyModule.psd1' + + Installs all modules defined in the manifest file, following PSModuleInfo structure. + + .NOTES + Should later be adapted to support both pre-reqs, and dependencies. + Should later be adapted to take 4 parameters sets: specific version ("requiredVersion" | "GUID"), latest version ModuleVersion, + and latest version within a range MinimumVersion - MaximumVersion. + #> + [Alias('Resolve-PSModuleDependencies')] + [CmdletBinding()] + param( + # The path to the manifest file. + [Parameter(Mandatory)] + [string] $ManifestFilePath + ) + + Write-Verbose 'Resolving dependencies' + + $manifest = Import-PowerShellDataFile -Path $ManifestFilePath + Write-Verbose "Reading [$ManifestFilePath]" + Write-Verbose "Found [$($manifest.RequiredModules.Count)] modules to install" + + foreach ($requiredModule in $manifest.RequiredModules) { + $installParams = @{} + + if ($requiredModule -is [string]) { + $installParams.Name = $requiredModule + } else { + $installParams.Name = $requiredModule.ModuleName + $installParams.MinimumVersion = $requiredModule.ModuleVersion + $installParams.RequiredVersion = $requiredModule.RequiredVersion + $installParams.MaximumVersion = $requiredModule.MaximumVersion + } + $installParams.Force = $true + $installParams.Verbose = $false + + Write-Verbose "[$($installParams.Name)] - Installing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Retry -Count 5 -Delay 10 { + Install-Module @installParams -AllowPrerelease:$false + } + $VerbosePreference = $VerbosePreferenceOriginal + Write-Verbose "[$($installParams.Name)] - Importing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Import-Module @installParams + $VerbosePreference = $VerbosePreferenceOriginal + Write-Verbose "[$($installParams.Name)] - Done" + } + Write-Verbose 'Resolving dependencies - Done' +} diff --git a/.github/actions/Publish/scripts/main.ps1 b/.github/actions/Publish/scripts/main.ps1 new file mode 100644 index 00000000..f6f0c4e2 --- /dev/null +++ b/.github/actions/Publish/scripts/main.ps1 @@ -0,0 +1,34 @@ +[CmdletBinding()] +param() + +$path = (Join-Path -Path $PSScriptRoot -ChildPath 'helpers') +LogGroup "Loading helper scripts from [$path]" { + Get-ChildItem -Path $path -Filter '*.ps1' -Recurse | ForEach-Object { + Write-Verbose "[$($_.FullName)]" + . $_.FullName + } +} + +LogGroup 'Loading inputs' { + Write-Verbose "Name: [$env:GITHUB_ACTION_INPUT_Name]" + Write-Verbose "GITHUB_REPOSITORY: [$env:GITHUB_REPOSITORY]" + Write-Verbose "GITHUB_WORKSPACE: [$env:GITHUB_WORKSPACE]" + + $name = ($env:GITHUB_ACTION_INPUT_Name | IsNullOrEmpty) ? $env:GITHUB_REPOSITORY_NAME : $env:GITHUB_ACTION_INPUT_Name + Write-Verbose "Module name: [$name]" + Write-Verbose "Module path: [$env:GITHUB_ACTION_INPUT_ModulePath]" + Write-Verbose "Doc path: [$env:GITHUB_ACTION_INPUT_DocsPath]" + + $modulePath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $env:GITHUB_ACTION_INPUT_ModulePath $name + Write-Verbose "Module path: [$modulePath]" + if (-not (Test-Path -Path $modulePath)) { + throw "Module path [$modulePath] does not exist." + } +} + +$params = @{ + Name = $name + ModulePath = $modulePath + APIKey = $env:GITHUB_ACTION_INPUT_APIKey +} +Publish-PSModule @params diff --git a/.github/actions/Test/action.yml b/.github/actions/Test/action.yml new file mode 100644 index 00000000..03e4bdd5 --- /dev/null +++ b/.github/actions/Test/action.yml @@ -0,0 +1,86 @@ +name: Test-PSModule (by PSModule) +description: Test a PowerShell module before publishing the module to the PowerShell Gallery. +author: PSModule +branding: + icon: check-square + color: gray-dark + +inputs: + Name: + description: The name of the module to test. The name of the repository is used if not specified. + required: false + Path: + description: The path to the code to test. + required: true + TestType: + description: The type of tests to run. Can be either 'Module' or 'SourceCode'. + required: true + TestsPath: + description: The path to the tests to run. + required: false + default: tests + StackTraceVerbosity: + description: "Verbosity level of the stack trace. Allowed values: 'None', 'FirstLine', 'Filtered', 'Full'." + required: false + default: 'Filtered' + Verbosity: + description: "Verbosity level of the test output. Allowed values: 'None', 'Normal', 'Detailed', 'Diagnostic'." + required: false + default: 'Detailed' + Debug: + description: Enable debug output. + required: false + default: 'false' + Verbose: + description: Enable verbose output. + required: false + default: 'false' + Version: + description: Specifies the version of the GitHub module to be installed. The value must be an exact version. + required: false + Prerelease: + description: Allow prerelease versions if available. + required: false + default: 'false' + +outputs: + passed: + description: If the tests passed. + value: ${{ fromJSON(steps.test.outputs.result).passed }} + +runs: + using: composite + steps: + - name: Run Test-PSModule + uses: PSModule/GitHub-Script@v1 + id: test + env: + GITHUB_ACTION_INPUT_Name: ${{ inputs.Name }} + GITHUB_ACTION_INPUT_Path: ${{ inputs.Path }} + GITHUB_ACTION_INPUT_TestType: ${{ inputs.TestType }} + GITHUB_ACTION_INPUT_TestsPath: ${{ inputs.TestsPath }} + GITHUB_ACTION_INPUT_StackTraceVerbosity: ${{ inputs.StackTraceVerbosity }} + GITHUB_ACTION_INPUT_Verbosity: ${{ inputs.Verbosity }} + with: + Debug: ${{ inputs.Debug }} + Prerelease: ${{ inputs.Prerelease }} + Verbose: ${{ inputs.Verbose }} + Version: ${{ inputs.Version }} + ShowOutput: true + Script: | + # Test-PSModule + ${{ github.action_path }}/scripts/main.ps1 + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: ${{ inputs.TestType == 'Module' && (success() || failure()) }} + with: + name: ${{ runner.os }}-Test-Report + path: ${{ github.workspace }}/outputs/Test-Report.xml + + - name: Upload code coverage report + uses: actions/upload-artifact@v4 + if: ${{ inputs.TestType == 'Module' && (success() || failure()) }} + with: + name: ${{ runner.os }}-CodeCoverage-Report + path: ${{ github.workspace }}/outputs/CodeCoverage-Report.xml diff --git a/.github/actions/Test/scripts/helpers/Resolve-PSModuleDependency.ps1 b/.github/actions/Test/scripts/helpers/Resolve-PSModuleDependency.ps1 new file mode 100644 index 00000000..8a6ddca8 --- /dev/null +++ b/.github/actions/Test/scripts/helpers/Resolve-PSModuleDependency.ps1 @@ -0,0 +1,64 @@ +#Requires -Modules Retry + +function Resolve-PSModuleDependency { + <# + .SYNOPSIS + Resolve dependencies for a module based on the manifest file. + + .DESCRIPTION + Resolve dependencies for a module based on the manifest file, following PSModuleInfo structure + + .EXAMPLE + Resolve-PSModuleDependency -Path 'C:\MyModule\MyModule.psd1' + + Installs all modules defined in the manifest file, following PSModuleInfo structure. + + .NOTES + Should later be adapted to support both pre-reqs, and dependencies. + Should later be adapted to take 4 parameters sets: specific version ("requiredVersion" | "GUID"), latest version ModuleVersion, + and latest version within a range MinimumVersion - MaximumVersion. + #> + [Alias('Resolve-PSModuleDependencies')] + [CmdletBinding()] + param( + # The path to the manifest file. + [Parameter(Mandatory)] + [string] $ManifestFilePath + ) + + Write-Host 'Resolving dependencies' + + $manifest = Import-PowerShellDataFile -Path $ManifestFilePath + Write-Host "Reading [$ManifestFilePath]" + Write-Host "Found [$($manifest.RequiredModules.Count)] modules to install" + + foreach ($requiredModule in $manifest.RequiredModules) { + $installParams = @{} + + if ($requiredModule -is [string]) { + $installParams.Name = $requiredModule + } else { + $installParams.Name = $requiredModule.ModuleName + $installParams.MinimumVersion = $requiredModule.ModuleVersion + $installParams.RequiredVersion = $requiredModule.RequiredVersion + $installParams.MaximumVersion = $requiredModule.MaximumVersion + } + $installParams.Force = $true + $installParams.Verbose = $false + + Write-Host "[$($installParams.Name)] - Installing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Retry -Count 5 -Delay 10 { + Install-Module @installParams -AllowPrerelease:$false + } + $VerbosePreference = $VerbosePreferenceOriginal + Write-Host "[$($installParams.Name)] - Importing module" + $VerbosePreferenceOriginal = $VerbosePreference + $VerbosePreference = 'SilentlyContinue' + Import-Module @installParams + $VerbosePreference = $VerbosePreferenceOriginal + Write-Host "[$($installParams.Name)] - Done" + } + Write-Host 'Resolving dependencies - Done' +} diff --git a/.github/actions/Test/scripts/helpers/Test-PSModule.ps1 b/.github/actions/Test/scripts/helpers/Test-PSModule.ps1 new file mode 100644 index 00000000..27d0e094 --- /dev/null +++ b/.github/actions/Test/scripts/helpers/Test-PSModule.ps1 @@ -0,0 +1,177 @@ +function Test-PSModule { + <# + .SYNOPSIS + Performs tests on a module. + #> + [OutputType([int])] + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', Scope = 'Function', + Justification = 'Parameters are used in nested ScriptBlocks' + )] + param( + # Path to the folder where the code to test is located. + [Parameter(Mandatory)] + [string] $Path, + + # Run module tests. + [Parameter()] + [ValidateSet('SourceCode', 'Module')] + [string] $TestType = 'SourceCode', + + # Path to the folder where the tests are located. + [Parameter()] + [string] $TestsPath = 'tests', + + # Verbosity level of the stack trace. + [Parameter()] + [ValidateSet('None', 'FirstLine', 'Filtered', 'Full')] + [string] $StackTraceVerbosity = 'Filtered', + + # Verbosity level of the test output. + [Parameter()] + [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] + [string] $Verbosity = 'Detailed' + ) + + $moduleName = Split-Path -Path $Path -Leaf + $testSourceCode = $TestType -eq 'SourceCode' + $testModule = $TestType -eq 'Module' + $moduleTestsPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $TestsPath + + LogGroup 'Get test kit versions' { + $PSSAModule = Get-PSResource -Name PSScriptAnalyzer -Verbose:$false | Sort-Object Version -Descending | Select-Object -First 1 + $pesterModule = Get-PSResource -Name Pester -Verbose:$false | Sort-Object Version -Descending | Select-Object -First 1 + + [PSCustomObject]@{ + PowerShell = $PSVersionTable.PSVersion.ToString() + Pester = $pesterModule.version + PSScriptAnalyzer = $PSSAModule.version + } | Format-List + } + + LogGroup 'Add test - Common - PSScriptAnalyzer' { + $containers = @() + $PSSATestsPath = Join-Path -Path $PSScriptRoot -ChildPath '..\tests\PSScriptAnalyzer' + $settingsFileName = if ($testModule) { 'Settings.Module.psd1' } else { 'Settings.SourceCode.psd1' } + $settingsFilePath = Join-Path -Path $PSSATestsPath -ChildPath $settingsFileName + $containerParams = @{ + Path = Join-Path $PSSATestsPath 'PSScriptAnalyzer.Tests.ps1' + Data = @{ + Path = $Path + SettingsFilePath = $settingsFilePath + Debug = $false + Verbose = $false + } + } + Write-Host ($containerParams | ConvertTo-Json) + $containers += New-PesterContainer @containerParams + } + + LogGroup 'Add test - Common - PSModule' { + $containerParams = @{ + Path = Join-Path -Path $PSScriptRoot -ChildPath '..\tests\PSModule\Common.Tests.ps1' + Data = @{ + Path = $Path + Debug = $false + Verbose = $false + } + } + Write-Host ($containerParams | ConvertTo-Json) + $containers += New-PesterContainer @containerParams + } + + if ($testModule) { + LogGroup 'Add test - Module - PSModule' { + $containerParams = @{ + Path = Join-Path -Path $PSScriptRoot -ChildPath '..\tests\PSModule\Module.Tests.ps1' + Data = @{ + Path = $Path + Debug = $false + Verbose = $false + } + } + Write-Host ($containerParams | ConvertTo-Json) + $containers += New-PesterContainer @containerParams + } + } + + if ($testSourceCode) { + LogGroup 'Add test - SourceCode - PSModule' { + $containerParams = @{ + Path = Join-Path -Path $PSScriptRoot -ChildPath '..\tests\PSModule\SourceCode.Tests.ps1' + Data = @{ + Path = $Path + TestsPath = $moduleTestsPath + Debug = $false + Verbose = $false + } + } + Write-Host ($containerParams | ConvertTo-Json) + $containers += New-PesterContainer @containerParams + } + } + + if ($testModule) { + if (Test-Path -Path $moduleTestsPath) { + LogGroup "Add test - Module - $moduleName" { + $containerParams = @{ + Path = $moduleTestsPath + } + Write-Host ($containerParams | ConvertTo-Json) + $containers += New-PesterContainer @containerParams + } + } else { + Write-GitHubWarning "⚠️ No tests found - [$moduleTestsPath]" + } + } + + if ((Test-Path -Path $moduleTestsPath) -and $testModule) { + LogGroup 'Install module dependencies' { + $moduleManifestPath = Join-Path -Path $Path -ChildPath "$moduleName.psd1" + Resolve-PSModuleDependency -ManifestFilePath $moduleManifestPath + } + + LogGroup "Importing module: $moduleName" { + Add-PSModulePath -Path (Split-Path $Path -Parent) + $existingModule = Get-Module -Name $ModuleName -ListAvailable + $existingModule | Remove-Module -Force + $existingModule.RequiredModules | ForEach-Object { $_ | Remove-Module -Force -ErrorAction SilentlyContinue } + $existingModule.NestedModules | ForEach-Object { $_ | Remove-Module -Force -ErrorAction SilentlyContinue } + Import-Module -Name $moduleName -Force -RequiredVersion '999.0.0' -Global + } + } + + LogGroup 'Pester config' { + $pesterParams = @{ + Configuration = @{ + Run = @{ + Path = $Path + Container = $containers + PassThru = $true + } + TestResult = @{ + Enabled = $testModule + OutputFormat = 'NUnitXml' + OutputPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath 'outputs\Test-Report.xml' + TestSuiteName = 'Unit tests' + } + CodeCoverage = @{ + Enabled = $testModule + OutputPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath 'outputs\CodeCoverage-Report.xml' + OutputFormat = 'JaCoCo' + OutputEncoding = 'UTF8' + CoveragePercentTarget = 75 + } + Output = @{ + CIFormat = 'Auto' + StackTraceVerbosity = $StackTraceVerbosity + Verbosity = $Verbosity + } + } + } + Write-Host ($pesterParams | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue) + } + + Invoke-Pester @pesterParams +} diff --git a/.github/actions/Test/scripts/main.ps1 b/.github/actions/Test/scripts/main.ps1 new file mode 100644 index 00000000..39d0bfda --- /dev/null +++ b/.github/actions/Test/scripts/main.ps1 @@ -0,0 +1,62 @@ +[CmdletBinding()] +param() + +$path = (Join-Path -Path $PSScriptRoot -ChildPath 'helpers') +LogGroup "Loading helper scripts from [$path]" { + Get-ChildItem -Path $path -Filter '*.ps1' -Recurse | ForEach-Object { + Write-Host " - $($_.FullName)" + . $_.FullName + } +} + +LogGroup 'Loading inputs' { + $moduleName = ($env:GITHUB_ACTION_INPUT_Name | IsNullOrEmpty) ? $env:GITHUB_REPOSITORY_NAME : $env:GITHUB_ACTION_INPUT_Name + $codeToTest = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath "$env:GITHUB_ACTION_INPUT_Path\$moduleName" + if (-not (Test-Path -Path $codeToTest)) { + $codeToTest = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath $env:GITHUB_ACTION_INPUT_Path + } + if (-not (Test-Path -Path $codeToTest)) { + throw "Path [$codeToTest] does not exist." + } + + if (-not (Test-Path -Path $env:GITHUB_ACTION_INPUT_TestsPath)) { + throw "Path [$env:GITHUB_ACTION_INPUT_TestsPath] does not exist." + } + + [pscustomobject]@{ + ModuleName = $moduleName + CodeToTest = $codeToTest + TestType = $env:GITHUB_ACTION_INPUT_TestType + TestsPath = $env:GITHUB_ACTION_INPUT_TestsPath + StackTraceVerbosity = $env:GITHUB_ACTION_INPUT_StackTraceVerbosity + Verbosity = $env:GITHUB_ACTION_INPUT_Verbosity + } | Format-List +} + +$params = @{ + Path = $codeToTest + TestType = $env:GITHUB_ACTION_INPUT_TestType + TestsPath = $env:GITHUB_ACTION_INPUT_TestsPath + StackTraceVerbosity = $env:GITHUB_ACTION_INPUT_StackTraceVerbosity + Verbosity = $env:GITHUB_ACTION_INPUT_Verbosity +} +$testResults = Test-PSModule @params + +LogGroup 'Test results' { + $testResults | Format-List +} + +$failedTests = [int]$testResults.FailedCount + +if (($failedTests -gt 0) -or ($testResults.Result -ne 'Passed')) { + Write-GitHubError "❌ Some [$failedTests] tests failed." + Set-GitHubOutput -Name 'passed' -Value $false + $return = 1 +} elseif ($failedTests -eq 0) { + Write-GitHubNotice '✅ All tests passed.' + Set-GitHubOutput -Name 'passed' -Value $true + $return = 0 +} + +Write-Host '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' +exit $return diff --git a/.github/actions/Test/scripts/tests/PSModule/Common.Tests.ps1 b/.github/actions/Test/scripts/tests/PSModule/Common.Tests.ps1 new file mode 100644 index 00000000..843e8b17 --- /dev/null +++ b/.github/actions/Test/scripts/tests/PSModule/Common.Tests.ps1 @@ -0,0 +1,42 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', 'Path', + Justification = 'Path is used to specify the path to the module to test.' +)] +[CmdLetBinding()] +Param( + [Parameter(Mandatory)] + [string] $Path +) + +# These tests are for the whole module and its parts. The scope of these tests are on the src folder and the specific module folder within it. +Describe 'Script files' { + Context 'Module design tests' { + # It 'Script file should only contain one function or filter' {} + # It 'All script files have tests' {} # Look for the folder name in tests called the same as section/folder name of functions + } + + Describe 'Function/filter design' { + # It 'comment based doc block start is indented with 4 spaces' {} + # It 'comment based doc is indented with 8 spaces' {} + # It 'has synopsis for all functions' {} + # It 'has description for all functions' {} + # It 'has examples for all functions' {} + # It 'has output documentation for all functions' {} + # It 'has [CmdletBinding()] attribute' {} + # It 'boolean parameters in CmdletBinding() attribute are written without assignments' {} + # I.e. [CmdletBinding(ShouldProcess)] instead of [CmdletBinding(ShouldProcess = $true)] + # It 'has [OutputType()] attribute' {} + # It 'has verb 'New','Set','Disable','Enable' etc. and uses "ShoudProcess" in the [CmdletBinding()] attribute' {} + } + + Describe 'Parameter design' { + # It 'has parameter description for all functions' {} + # It 'has parameter validation for all functions' {} + # It 'parameters have [Parameters()] attribute' {} + # It 'boolean parameters to the [Parameter()] attribute are written without assignments' {} + # I.e. [Parameter(Mandatory)] instead of [Parameter(Mandatory = $true)] + # It 'datatype for parameters are written on the same line as the parameter name' {} + # It 'datatype for parameters and parameter name are separated by a single space' {} + # It 'parameters are separated by a blank line' {} + } +} diff --git a/.github/actions/Test/scripts/tests/PSModule/Module.Tests.ps1 b/.github/actions/Test/scripts/tests/PSModule/Module.Tests.ps1 new file mode 100644 index 00000000..772f2a5b --- /dev/null +++ b/.github/actions/Test/scripts/tests/PSModule/Module.Tests.ps1 @@ -0,0 +1,50 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', 'Path', + Justification = 'Path is used to specify the path to the module to test.' +)] +[CmdLetBinding()] +Param( + [Parameter(Mandatory)] + [string] $Path +) + +BeforeAll { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'moduleName', + Justification = 'moduleName is used in the test.' + )] + $moduleName = Split-Path -Path $Path -Leaf +} + +Describe 'PSModule - Module tests' { + Context 'Module' { + It 'The module should be available' { + Get-Module -Name $moduleName -ListAvailable | Should -Not -BeNullOrEmpty + Write-Verbose (Get-Module -Name $moduleName -ListAvailable | Out-String) + } + It 'The module should be importable' { + { Import-Module -Name $moduleName -RequiredVersion 999.0.0 -Force } | Should -Not -Throw + } + } + + Context "Module Manifest" { + BeforeAll { + $moduleManifestPath = Join-Path -Path $Path -ChildPath "$moduleName.psd1" + Write-Verbose "Module Manifest Path: [$moduleManifestPath]" + } + It 'Module Manifest exists' { + $result = Test-Path -Path $moduleManifestPath + $result | Should -Be $true + Write-Verbose $result + } + It 'Module Manifest is valid' { + $result = Test-ModuleManifest -Path $moduleManifestPath + $result | Should -Not -Be $null + Write-Verbose $result + } + # It 'has a valid license URL' {} + # It 'has a valid project URL' {} + # It 'has a valid icon URL' {} + # It 'has a valid help URL' {} + } +} diff --git a/.github/actions/Test/scripts/tests/PSModule/SourceCode.Tests.ps1 b/.github/actions/Test/scripts/tests/PSModule/SourceCode.Tests.ps1 new file mode 100644 index 00000000..bbed2449 --- /dev/null +++ b/.github/actions/Test/scripts/tests/PSModule/SourceCode.Tests.ps1 @@ -0,0 +1,385 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', '', + Justification = 'Parameters are used in the test.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'functionBearingPublicFiles', + Justification = 'Variables are used in the test.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'functionBearingFiles', + Justification = 'Variables are used in the test.' +)] +[CmdLetBinding()] +Param( + # The path to the 'src' folder of the repo. + [Parameter(Mandatory)] + [string] $Path, + + # The path to the 'tests' folder of the repo. + [Parameter(Mandatory)] + [string] $TestsPath +) + +BeforeAll { + $scriptFiles = Get-ChildItem -Path $Path -Include *.psm1, *.ps1 -Recurse -File + LogGroup "Found $($scriptFiles.Count) script files in [$Path]" { + $scriptFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } + $functionsPath = Join-Path -Path $Path -ChildPath 'functions' + $functionFiles = (Test-Path -Path $functionsPath) ? (Get-ChildItem -Path $functionsPath -File -Filter '*.ps1' -Recurse) : $null + + LogGroup "Found $($functionFiles.Count) function files in [$functionsPath]" { + $functionFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } + $privateFunctionsPath = Join-Path -Path $functionsPath -ChildPath 'private' + $privateFunctionFiles = (Test-Path -Path $privateFunctionsPath) ? + (Get-ChildItem -Path $privateFunctionsPath -File -Filter '*.ps1' -Recurse) : $null + LogGroup "Found $($privateFunctionFiles.Count) private function files in [$privateFunctionsPath]" { + $privateFunctionFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } + $publicFunctionsPath = Join-Path -Path $functionsPath -ChildPath 'public' + $publicFunctionFiles = (Test-Path -Path $publicFunctionsPath) ? (Get-ChildItem -Path $publicFunctionsPath -File -Filter '*.ps1' -Recurse) : $null + LogGroup "Found $($publicFunctionFiles.Count) public function files in [$publicFunctionsPath]" { + $publicFunctionFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } + $variablesPath = Join-Path -Path $Path -ChildPath 'variables' + $variableFiles = (Test-Path -Path $variablesPath) ? (Get-ChildItem -Path $variablesPath -File -Filter '*.ps1' -Recurse) : $null + LogGroup "Found $($variableFiles.Count) variable files in [$variablesPath]" { + $variableFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } + $privateVariablesPath = Join-Path -Path $variablesPath -ChildPath 'private' + $privateVariableFiles = (Test-Path -Path $privateVariablesPath) ? + (Get-ChildItem -Path $privateVariablesPath -File -Filter '*.ps1' -Recurse) : $null + LogGroup "Found $($privateVariableFiles.Count) private variable files in [$privateVariablesPath]" { + $privateVariableFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } + $publicVariablesPath = Join-Path -Path $variablesPath -ChildPath 'public' + $publicVariableFiles = (Test-Path -Path $publicVariablesPath) ? + (Get-ChildItem -Path $publicVariablesPath -File -Filter '*.ps1' -Recurse) : $null + LogGroup "Found $($publicVariableFiles.Count) public variable files in [$publicVariablesPath]" { + $publicVariableFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } + $classPath = Join-Path -Path $Path -ChildPath 'classes' + $classFiles = (Test-Path -Path $classPath) ? (Get-ChildItem -Path $classPath -File -Filter '*.ps1' -Recurse) : $null + LogGroup "Found $($classFiles.Count) class files in [$classPath]" { + $classFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } + $privateClassPath = Join-Path -Path $classPath -ChildPath 'private' + $privateClassFiles = (Test-Path -Path $privateClassPath) ? + (Get-ChildItem -Path $privateClassPath -File -Filter '*.ps1' -Recurse) : $null + LogGroup "Found $($privateClassFiles.Count) private class files in [$privateClassPath]" { + $privateClassFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } + $publicClassPath = Join-Path -Path $classPath -ChildPath 'public' + $publicClassFiles = (Test-Path -Path $publicClassPath) ? + (Get-ChildItem -Path $publicClassPath -File -Filter '*.ps1' -Recurse) : $null + LogGroup "Found $($publicClassFiles.Count) public class files in [$publicClassPath]" { + $publicClassFiles | ForEach-Object { + Write-Verbose " - $($_.FullName)" -Verbose + } + } +} + +Describe 'PSModule - SourceCode tests' { + Context 'General tests' { + It "Should use '[System.Environment]::ProcessorCount' instead of '`$env:NUMBER_OF_PROCESSORS' (ID: NumberOfProcessors)" { + $issues = @('') + $scriptFiles | ForEach-Object { + Select-String -Path $_.FullName -Pattern '\$env:NUMBER_OF_PROCESSORS' -AllMatches | ForEach-Object { + $filePath = $_.FullName + $relativePath = $filePath.Replace($Path, '').Trim('\').Trim('/') + $skipTest = Select-String -Path $filePath -Pattern '#SkipTest:NumberOfProcessors:(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping NumberOfProcessors test' + } else { + $issues += " - $($_.Path):L$($_.LineNumber)" + } + } + } + $issues -join [Environment]::NewLine | + Should -BeNullOrEmpty -Because 'the script should use [System.Environment]::ProcessorCount instead of $env:NUMBER_OF_PROCESSORS' + } + It "Should not contain '-Verbose' unless it is disabled using ':`$false' qualifier after it (ID: Verbose)" { + $issues = @('') + $scriptFiles | ForEach-Object { + $filePath = $_.FullName + $relativePath = $filePath.Replace($Path, '').Trim('\').Trim('/') + $skipTest = Select-String -Path $filePath -Pattern '#SkipTest:Verbose:(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping Verbose test' + } else { + Select-String -Path $filePath -Pattern '\s(-Verbose(?::\$true)?)\b(?!:\$false)' -AllMatches | ForEach-Object { + $issues += " - $relativePath`:L$($_.LineNumber) - $($_.Line)" + } + } + } + $issues -join [Environment]::NewLine | + Should -BeNullOrEmpty -Because "the script should not contain '-Verbose' unless it is disabled using ':`$false' qualifier after it." + } + It "Should use '`$null = ...' instead of '... | Out-Null' (ID: OutNull)" { + $issues = @('') + $scriptFiles | ForEach-Object { + $filePath = $_.FullName + $relativePath = $filePath.Replace($Path, '').Trim('\').Trim('/') + $skipTest = Select-String -Path $filePath -Pattern '#SkipTest:OutNull:(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping OutNull test' + } else { + Select-String -Path $filePath -Pattern 'Out-Null' -AllMatches | ForEach-Object { + $issues += " - $relativePath`:L$($_.LineNumber) - $($_.Line)" + } + } + } + $issues -join [Environment]::NewLine | + Should -BeNullOrEmpty -Because "the script should use '`$null = ...' instead of '... | Out-Null'" + } + It 'Should not use ternary operations for compatability reasons (ID: NoTernary)' -Skip { + $issues = @('') + $scriptFiles | ForEach-Object { + $filePath = $_.FullName + $relativePath = $filePath.Replace($Path, '').Trim('\').Trim('/') + $skipTest = Select-String -Path $filePath -Pattern '#SkipTest:NoTernary:(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping NoTernary test' + } else { + Select-String -Path $filePath -Pattern '(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping LowercaseKeywords test' + } else { + $errors = $null + $tokens = $null + [System.Management.Automation.Language.Parser]::ParseFile($FilePath, [ref]$tokens, [ref]$errors) + + foreach ($token in $tokens) { + $keyword = $token.Text + $lineNumber = $token.Extent.StartLineNumber + $columnNumber = $token.Extent.StartColumnNumber + if (($token.TokenFlags -match 'Keyword') -and ($keyword -cne $keyword.ToLower())) { + $issues += " - $relativePath`:L$lineNumber`:C$columnNumber - $keyword" + } + } + } + } + $issues -join [Environment]::NewLine | Should -BeNullOrEmpty -Because 'all powershell keywords should be lowercase' + } + } + + Context 'classes' { + } + + Context 'functions' { + Context 'Generic' { + BeforeAll { + $functionBearingFiles = $functionFiles | Where-Object { + $Ast = [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$null, [ref]$null) + $tokens = $Ast.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } , $true ) + $tokens.count -ne 0 + } + } + # It 'has synopsis for all functions' {} + # It 'has description for all functions' {} + # It 'has examples for all functions' {} + # It 'comment based doc is indented with 8 spaces' {} + # It 'boolean parameters in CmdletBinding() attribute are written without assignments' {} + # I.e. [CmdletBinding(ShouldProcess)] instead of [CmdletBinding(ShouldProcess = $true)] + # It 'has [OutputType()] attribute' {} + # Parameters + # It 'comment based doc block start is indented with 4 spaces' {} + # It 'has parameter description for all functions' {} + # It 'parameters have [Parameter()] attribute' {} + # It 'boolean parameters to the [Parameter()] attribute are written without assignments' {} + # I.e. [Parameter(Mandatory)] instead of [Parameter(Mandatory = $true)] + # It 'datatype for parameters are written on the same line as the parameter name' {} + # It 'datatype for parameters and parameter name are separated by a single space' {} + # It 'parameters are separated by a blank line' {} + It 'Should contain one function or filter (ID: FunctionCount)' { + $issues = @('') + $functionBearingFiles | ForEach-Object { + $filePath = $_.FullName + $relativePath = $filePath.Replace($Path, '').Trim('\').Trim('/') + $skipTest = Select-String -Path $filePath -Pattern '#SkipTest:FunctionCount:(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping FunctionCount test' + } else { + $Ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$null, [ref]$null) + $tokens = $Ast.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } , $true ) + if ($tokens.count -ne 1) { + $issues += " - $relativePath - $($tokens.Name)" + } + } + } + $issues -join [Environment]::NewLine | + Should -BeNullOrEmpty -Because 'the script should contain one function or filter' + } + It 'Should have matching filename and function/filter name (ID: FunctionName)' { + $issues = @('') + $functionBearingFiles | ForEach-Object { + $filePath = $_.FullName + $fileName = $_.BaseName + $relativePath = $filePath.Replace($Path, '').Trim('\').Trim('/') + $skipTest = Select-String -Path $filePath -Pattern '#SkipTest:FunctionName:(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping FunctionName test' + } else { + $Ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$null, [ref]$null) + $tokens = $Ast.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } , $true ) + if ($tokens.Name -ne $fileName) { + $issues += " - $relativePath - $($tokens.Name)" + } + } + } + $issues -join [Environment]::NewLine | + Should -BeNullOrEmpty -Because 'the script files should be called the same as the function they contain' + } + It 'Should have [CmdletBinding()] attribute (ID: CmdletBinding)' { + $issues = @('') + $functionBearingFiles | ForEach-Object { + $found = $false + $filePath = $_.FullName + $relativePath = $filePath.Replace($Path, '').Trim('\').Trim('/') + $skipTest = Select-String -Path $filePath -Pattern '#SkipTest:CmdletBinding:(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping CmdletBinding test' + } else { + $scriptAst = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$null, [ref]$null) + $tokens = $scriptAst.FindAll({ $true }, $true) + foreach ($token in $tokens) { + if ($token.TypeName.Name -eq 'CmdletBinding') { + $found = $true + } + } + if (-not $found) { + $issues += " - $relativePath" + } + } + } + $issues -join [Environment]::NewLine | + Should -BeNullOrEmpty -Because 'the script should have [CmdletBinding()] attribute' + } + It 'Should have a param() block (ID: ParamBlock)' { + $issues = @('') + $functionBearingFiles | ForEach-Object { + $found = $false + $filePath = $_.FullName + $relativePath = $filePath.Replace($Path, '').Trim('\').Trim('/') + $skipTest = Select-String -Path $filePath -Pattern '#SkipTest:ParamBlock:(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping ParamBlock test' + } else { + $scriptAst = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$null, [ref]$null) + $tokens = $scriptAst.FindAll({ $args[0] -is [System.Management.Automation.Language.ParamBlockAst] }, $true) + foreach ($token in $tokens) { + if ($token.count -eq 1) { + $found = $true + } + } + if (-not $found) { + $issues += " - $relativePath" + } + } + } + $issues -join [Environment]::NewLine | + Should -BeNullOrEmpty -Because 'the script should have a param() block' + } + } + Context 'public functions' { + BeforeAll { + $functionBearingPublicFiles = $publicFunctionFiles | Where-Object { + $Ast = [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$null, [ref]$null) + $tokens = $Ast.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } , $true ) + $tokens.count -ne 0 + } + } + It 'All public functions/filters have tests (ID: FunctionTest)' { + $issues = @('') + + # Get commands used in tests from the files in 'tests' folder. + $testFiles = Get-ChildItem -Path $TestsPath -Recurse -File -Filter '*.ps1' + $functionsInTestFiles = $testFiles | ForEach-Object { + $ast = [System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$null, [ref]$null) + $ast.FindAll( + { + param($node) + $node -is [System.Management.Automation.Language.CommandAst] -and + $node.GetCommandName() -ne $null + }, + $true + ) | ForEach-Object { + $_.GetCommandName() + } | Sort-Object -Unique + } + + # Get all the functions in the public function files and check if they have a test. + $functionBearingPublicFiles | ForEach-Object { + $filePath = $_.FullName + $relativePath = $filePath.Replace($Path, '').Trim('\').Trim('/') + $skipTest = Select-String -Path $filePath -Pattern '#SkipTest:FunctionTest:(?.+)' -AllMatches + if ($skipTest.Matches.Count -gt 0) { + $skipReason = $skipTest.Matches.Groups | Where-Object { $_.Name -eq 'Reason' } | Select-Object -ExpandProperty Value + Write-GitHubWarning -Message " - $relativePath - $skipReason" -Title 'Skipping FunctionTest test' + } else { + $Ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$null, [ref]$null) + $tokens = $Ast.FindAll( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } , $true ) + $functionName = $tokens.Name + # If the file contains a function and the function name is not in the test files, add it as an issue. + if ($functionName.count -eq 1 -and $functionsInTestFiles -notcontains $functionName) { + $issues += " - $relativePath - $functionName" + } + } + } + $issues -join [Environment]::NewLine | + Should -BeNullOrEmpty -Because 'a test should exist for each of the functions in the module' + } + } + Context 'private functions' {} + } + + Context 'variables' { + } + + Context 'Module manifest' { + # It 'Module Manifest exists (maifest.psd1 or modulename.psd1)' {} + # It 'Module Manifest is valid' {} + } +} diff --git a/.github/actions/Test/scripts/tests/PSScriptAnalyzer/PSScriptAnalyzer.Tests.ps1 b/.github/actions/Test/scripts/tests/PSScriptAnalyzer/PSScriptAnalyzer.Tests.ps1 new file mode 100644 index 00000000..2a6039b8 --- /dev/null +++ b/.github/actions/Test/scripts/tests/PSScriptAnalyzer/PSScriptAnalyzer.Tests.ps1 @@ -0,0 +1,55 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', 'Path', + Justification = 'Path is being used.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', 'SettingsFilePath', + Justification = 'SettingsFilePath is being used.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'relativeSettingsFilePath', + Justification = 'relativeSettingsFilePath is being used.' +)] +[CmdLetBinding()] +Param( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter(Mandatory)] + [string] $SettingsFilePath +) + +BeforeDiscovery { + $rules = [Collections.Generic.List[System.Collections.Specialized.OrderedDictionary]]::new() + $ruleObjects = Get-ScriptAnalyzerRule -Verbose:$false | Sort-Object -Property Severity, CommonName + foreach ($ruleObject in $ruleObjects) { + $rules.Add( + [ordered]@{ + RuleName = $ruleObject.RuleName + CommonName = $ruleObject.CommonName + Severity = $ruleObject.Severity + Description = $ruleObject.Description + } + ) + } + Write-Warning "Discovered [$($rules.Count)] rules" + $relativeSettingsFilePath = $SettingsFilePath.Replace($PSScriptRoot, '').Trim('\').Trim('/') +} + +Describe "PSScriptAnalyzer tests using settings file [$relativeSettingsFilePath]" { + BeforeAll { + $testResults = Invoke-ScriptAnalyzer -Path $Path -Settings $SettingsFilePath -Recurse -Verbose:$false + Write-Warning "Found [$($testResults.Count)] issues" + } + + Context 'Severity: <_>' -ForEach 'Error', 'Warning', 'Information' { + It ' ()' -ForEach ($rules | Where-Object -Property Severity -EQ $_) { + $issues = [Collections.Generic.List[string]]::new() + $testResults | Where-Object -Property RuleName -EQ $RuleName | ForEach-Object { + $relativePath = $_.ScriptPath.Replace($Path, '').Trim('\').Trim('/') + $issues.Add(([Environment]::NewLine + " - $relativePath`:L$($_.Line):C$($_.Column)")) + } + $issues -join '' | Should -BeNullOrEmpty -Because $Description + } + } +} diff --git a/.github/actions/Test/scripts/tests/PSScriptAnalyzer/Settings.Module.psd1 b/.github/actions/Test/scripts/tests/PSScriptAnalyzer/Settings.Module.psd1 new file mode 100644 index 00000000..7c3a739c --- /dev/null +++ b/.github/actions/Test/scripts/tests/PSScriptAnalyzer/Settings.Module.psd1 @@ -0,0 +1,47 @@ +@{ + Rules = @{ + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + PSAvoidLongLines = @{ + Enable = $true + MaximumLineLength = 150 + } + PSAvoidSemicolonsAsLineTerminators = @{ + Enable = $true + } + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $false + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + PSProvideCommentHelp = @{ + Enable = $true + ExportedOnly = $false + BlockComment = $true + VSCodeSnippetCorrection = $false + Placement = 'begin' + } + PSUseConsistentIndentation = @{ + Enable = $true + IndentationSize = 4 + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + Kind = 'space' + } + PSUseConsistentWhitespace = @{ + Enable = $false + } + } + ExcludeRules = @( + 'PSAvoidUsingCmdletAliases', + 'PSUseToExportFieldsInManifest' + ) +} diff --git a/.github/actions/Test/scripts/tests/PSScriptAnalyzer/Settings.SourceCode.psd1 b/.github/actions/Test/scripts/tests/PSScriptAnalyzer/Settings.SourceCode.psd1 new file mode 100644 index 00000000..e9081f9a --- /dev/null +++ b/.github/actions/Test/scripts/tests/PSScriptAnalyzer/Settings.SourceCode.psd1 @@ -0,0 +1,57 @@ +@{ + Rules = @{ + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + PSAvoidLongLines = @{ + Enable = $true + MaximumLineLength = 150 + } + PSAvoidSemicolonsAsLineTerminators = @{ + Enable = $true + } + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $false + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + PSProvideCommentHelp = @{ + Enable = $true + ExportedOnly = $false + BlockComment = $true + VSCodeSnippetCorrection = $false + Placement = 'begin' + } + PSUseConsistentIndentation = @{ + Enable = $true + IndentationSize = 4 + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + Kind = 'space' + } + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $true + CheckSeparator = $true + CheckParameter = $true + IgnoreAssignmentOperatorInsideHashTable = $true + } + } + ExcludeRules = @( + 'PSMissingModuleManifestField', # This rule is not applicable until the module is built. + 'PSAvoidUsingCmdletAliases', + 'PSUseToExportFieldsInManifest' + ) +} diff --git a/.github/workflows/Aciton-Test-Publish.yml b/.github/workflows/Aciton-Test-Publish.yml new file mode 100644 index 00000000..1a9b0811 --- /dev/null +++ b/.github/workflows/Aciton-Test-Publish.yml @@ -0,0 +1,40 @@ +name: Aciton-Test-Publish + +run-name: "Aciton-Test-Publish - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: + workflow_dispatch: + pull_request: + schedule: + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: read + +jobs: + ActionTestDefault: + name: Aciton-Test-Publish - [Default] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize + + - name: Aciton-Test-Publish + uses: ./.github/actions/Publish + env: + GITHUB_TOKEN: ${{ github.token }} + with: + Verbose: true + Debug: true + Name: PSModuleTest + ModulePath: tests/outputTestRepo/outputs/modules + APIKey: ${{ secrets.APIKEY }} + WhatIf: true diff --git a/.github/workflows/Action-Test-Build.yml b/.github/workflows/Action-Test-Build.yml new file mode 100644 index 00000000..072a4a88 --- /dev/null +++ b/.github/workflows/Action-Test-Build.yml @@ -0,0 +1,57 @@ +name: Action-Test-Build + +run-name: "Action-Test-Build - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: + workflow_dispatch: + pull_request: + schedule: + - cron: '0 0 * * *' + +env: + GH_TOKEN: ${{ github.token }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + ActionTestDefault: + name: Action-Test-Build - [Default] + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize + + - name: Action-Test-Build + uses: ./.github/actions/Build + with: + Name: PSModuleTest + Path: tests/srcTestRepo/src + ModulesOutputPath: tests/srcTestRepo/outputs/modules + DocsOutputPath: tests/srcTestRepo/outputs/docs + + ActionTestWithManifest: + name: Action-Test-Build - [DefaultWithManifest] + runs-on: ubuntu-24.04 + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize + + - name: Action-Test-Build + uses: ./.github/actions/Build + with: + Name: PSModuleTest + Path: tests/srcWithManifestTestRepo/src + ModulesOutputPath: tests/srcWithManifestTestRepo/outputs/modules + DocsOutputPath: tests/srcWithManifestTestRepo/outputs/docs + ModuleArtifactName: moduleWithManifest + DocsArtifactName: docsWithManifest diff --git a/.github/workflows/Action-Test-Initialize.yml b/.github/workflows/Action-Test-Initialize.yml new file mode 100644 index 00000000..df99df41 --- /dev/null +++ b/.github/workflows/Action-Test-Initialize.yml @@ -0,0 +1,45 @@ +name: Action-Test-Initialize + +run-name: "Action-Test-Initialize - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: + workflow_dispatch: + pull_request: + schedule: + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + ActionTestBasic: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + name: Action-Test-Initialize - [Basic] - [${{ matrix.os }}] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Action-Test-Initialize + uses: ./.github/actions/Initialize + + - name: Run GitHub-script + uses: PSModule/GitHub-script@v1 + with: + Script: | + LogGroup "Run custom script" { + Write-Host "Hello, World!" + } + + - name: Run custom script + shell: pwsh + run: | + LogGroup "Run custom script" { + Write-Host "Hello, World!" + } diff --git a/.github/workflows/Action-Test-Test-Src-Default.yml b/.github/workflows/Action-Test-Test-Src-Default.yml new file mode 100644 index 00000000..b472c5ea --- /dev/null +++ b/.github/workflows/Action-Test-Test-Src-Default.yml @@ -0,0 +1,50 @@ +name: Action-Test-Test-Src-Default + +run-name: "Action-Test-Test-Src-Default - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: + workflow_dispatch: + pull_request: + schedule: + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + ActionTest: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + name: Action-Test-Test-Src-Default - [${{ matrix.os }}] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize + + - name: Action-Test-Test-outputs + uses: ./.github/actions/Test + id: test + env: + GITHUB_TOKEN: ${{ github.token }} + with: + Name: PSModuleTest + Path: tests/srcTestRepo/src + TestType: SourceCode + + - name: Status + shell: pwsh + env: + PASSED: ${{ steps.test.outputs.passed }} + run: | + Write-Host "Passed: [$env:PASSED]" + if ($env:PASSED -ne 'true') { + exit 1 + } diff --git a/.github/workflows/Action-Test-Test-Src-WithManifest.yml b/.github/workflows/Action-Test-Test-Src-WithManifest.yml new file mode 100644 index 00000000..45aa4e4b --- /dev/null +++ b/.github/workflows/Action-Test-Test-Src-WithManifest.yml @@ -0,0 +1,50 @@ +name: Action-Test [Src-WithManifest] + +run-name: "Action-Test [Src-WithManifest] - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: + workflow_dispatch: + pull_request: + schedule: + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + ActionTest: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + name: Action-Test [Src-WithManifest] - [${{ matrix.os }}] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize + + - name: Action-Test + uses: ./.github/actions/Test + id: test + env: + GITHUB_TOKEN: ${{ github.token }} + with: + Name: PSModuleTest + Path: tests/srcWithManifestTestRepo/src + TestType: SourceCode + + - name: Status + shell: pwsh + env: + PASSED: ${{ steps.test.outputs.passed }} + run: | + Write-Host "Passed: [$env:PASSED]" + if ($env:PASSED -ne 'true') { + exit 1 + } diff --git a/.github/workflows/Action-Test-Test-outputs.yml b/.github/workflows/Action-Test-Test-outputs.yml new file mode 100644 index 00000000..2258a32e --- /dev/null +++ b/.github/workflows/Action-Test-Test-outputs.yml @@ -0,0 +1,59 @@ +name: Action-Test-Test-outputs + +run-name: "Action-Test-Test-outputs - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: + workflow_dispatch: + pull_request: + schedule: + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +env: + TEST_APP_ENT_CLIENT_ID: ${{ secrets.TEST_APP_ENT_CLIENT_ID }} + TEST_APP_ENT_PRIVATE_KEY: ${{ secrets.TEST_APP_ENT_PRIVATE_KEY }} + TEST_APP_ORG_CLIENT_ID: ${{ secrets.TEST_APP_ORG_CLIENT_ID }} + TEST_APP_ORG_PRIVATE_KEY: ${{ secrets.TEST_APP_ORG_PRIVATE_KEY }} + TEST_USER_ORG_FG_PAT: ${{ secrets.TEST_USER_ORG_FG_PAT }} + TEST_USER_USER_FG_PAT: ${{ secrets.TEST_USER_USER_FG_PAT }} + TEST_USER_PAT: ${{ secrets.TEST_USER_PAT }} + +jobs: + ActionTest: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + name: Action-Test-Test-outputs - [${{ matrix.os }}] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize + + - name: Action-Test-Test-outputs + uses: ./.github/actions/Test + id: test + env: + GITHUB_TOKEN: ${{ github.token }} + with: + Name: PSModuleTest + Path: tests/outputTestRepo/outputs/modules + TestType: Module + + - name: Status + shell: pwsh + env: + PASSED: ${{ steps.test.outputs.passed }} + run: | + Write-Host "Passed: [$env:PASSED]" + if ($env:PASSED -ne 'true') { + exit 1 + } diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4822691c..04d2fbb0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -111,7 +111,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -120,7 +120,7 @@ jobs: - name: Test source code id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test with: Name: ${{ inputs.Name }} Path: ${{ inputs.Path }} @@ -143,7 +143,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -152,7 +152,7 @@ jobs: - name: Test source code id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test with: Name: ${{ inputs.Name }} Path: ${{ inputs.Path }} @@ -175,7 +175,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -184,7 +184,7 @@ jobs: - name: Test source code id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test with: Name: ${{ inputs.Name }} Path: ${{ inputs.Path }} @@ -211,7 +211,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -219,7 +219,7 @@ jobs: Version: ${{ inputs.Version }} - name: Build module - uses: PSModule/Build-PSModule@v2 + uses: ./.github/actions/Build with: Name: ${{ inputs.Name }} Path: ${{ inputs.Path }} @@ -247,7 +247,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -262,7 +262,7 @@ jobs: - name: Test built module id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test continue-on-error: true with: Name: ${{ inputs.Name }} @@ -299,7 +299,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -314,7 +314,7 @@ jobs: - name: Test built module id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test continue-on-error: true with: Name: ${{ inputs.Name }} @@ -351,7 +351,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -366,7 +366,7 @@ jobs: - name: Test built module id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test continue-on-error: true with: Name: ${{ inputs.Name }} @@ -404,7 +404,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -572,7 +572,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml new file mode 100644 index 00000000..50c152ba --- /dev/null +++ b/.github/workflows/Docs.yml @@ -0,0 +1,60 @@ +name: Publish Docs + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - docs/** + - mkdocs.yml + - .github/workflows/Docs.yml + +permissions: + contents: read # to checkout the repo + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + +env: + GH_TOKEN: ${{ github.token }} + +defaults: + run: + shell: pwsh + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/configure-pages@v5 + + - name: Install mkdoks-material + run: | + pip install mkdocs-material + pip install mkdocs-git-authors-plugin + pip install mkdocs-git-revision-date-localized-plugin + pip install mkdocs-git-committers-plugin-2 + + - name: Build mkdocs-material project + run: | + mkdocs build --config-file ./mkdocs.yml --strict --site-dir _site/ + + - uses: actions/upload-pages-artifact@v3 + + deploy: + needs: build + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/Workflow-Test-Default-CI.yml b/.github/workflows/Workflow-Test-Default-CI.yml index 4828c258..7f91a079 100644 --- a/.github/workflows/Workflow-Test-Default-CI.yml +++ b/.github/workflows/Workflow-Test-Default-CI.yml @@ -23,7 +23,7 @@ jobs: secrets: inherit with: Name: PSModuleTest - Path: tests/src - ModulesOutputPath: tests/outputs/modules - DocsOutputPath: tests/outputs/docs + Path: tests/srcTestRepo/src + ModulesOutputPath: tests/srcTestRepo/outputs/modules + DocsOutputPath: tests/srcTestRepo/outputs/docs SkipTests: Module diff --git a/.github/workflows/Workflow-Test-Default.yml b/.github/workflows/Workflow-Test-Default.yml index 2c72d2bb..3ae91ac9 100644 --- a/.github/workflows/Workflow-Test-Default.yml +++ b/.github/workflows/Workflow-Test-Default.yml @@ -25,8 +25,8 @@ jobs: secrets: inherit with: Name: PSModuleTest - Path: tests/src - ModulesOutputPath: tests/outputs/modules - DocsOutputPath: tests/outputs/docs + Path: tests/srcTestRepo/src + ModulesOutputPath: tests/srcTestRepo/outputs/modules + DocsOutputPath: tests/srcTestRepo/outputs/docs TestProcess: true SkipTests: Module diff --git a/.github/workflows/Workflow-Test-WithManifest-CI.yml b/.github/workflows/Workflow-Test-WithManifest-CI.yml index e998ec66..663c7e5e 100644 --- a/.github/workflows/Workflow-Test-WithManifest-CI.yml +++ b/.github/workflows/Workflow-Test-WithManifest-CI.yml @@ -23,7 +23,7 @@ jobs: secrets: inherit with: Name: PSModuleTest - Path: tests/srcWithManifest - ModulesOutputPath: tests/outputs/modules - DocsOutputPath: tests/outputs/docs + Path: tests/srcWithManifestTestRepo/src + ModulesOutputPath: tests/srcWithManifestTestRepo/outputs/modules + DocsOutputPath: tests/srcWithManifestTestRepo/outputs/docs SkipTests: Linux diff --git a/.github/workflows/Workflow-Test-WithManifest.yml b/.github/workflows/Workflow-Test-WithManifest.yml index ea4d4fdf..40034ff2 100644 --- a/.github/workflows/Workflow-Test-WithManifest.yml +++ b/.github/workflows/Workflow-Test-WithManifest.yml @@ -25,8 +25,8 @@ jobs: secrets: inherit with: Name: PSModuleTest - Path: tests/srcWithManifest - ModulesOutputPath: tests/outputs/modules - DocsOutputPath: tests/outputs/docs + Path: tests/srcWithManifestTestRepo/src + ModulesOutputPath: tests/srcWithManifestTestRepo/outputs/modules + DocsOutputPath: tests/srcWithManifestTestRepo/outputs/docs TestProcess: true SkipTests: Linux diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 9f2c7cba..b2d72b50 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -118,7 +118,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -127,7 +127,7 @@ jobs: - name: Test source code id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test with: Name: ${{ inputs.Name }} Path: ${{ inputs.Path }} @@ -150,7 +150,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -159,7 +159,7 @@ jobs: - name: Test source code id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test with: Name: ${{ inputs.Name }} Path: ${{ inputs.Path }} @@ -182,7 +182,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -191,7 +191,7 @@ jobs: - name: Test source code id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test with: Name: ${{ inputs.Name }} Path: ${{ inputs.Path }} @@ -218,7 +218,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -226,7 +226,7 @@ jobs: Version: ${{ inputs.Version }} - name: Build module - uses: PSModule/Build-PSModule@v2 + uses: ./.github/actions/Build with: Name: ${{ inputs.Name }} Path: ${{ inputs.Path }} @@ -254,7 +254,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -269,7 +269,7 @@ jobs: - name: Test built module id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test continue-on-error: true with: Name: ${{ inputs.Name }} @@ -306,7 +306,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -321,7 +321,7 @@ jobs: - name: Test built module id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test continue-on-error: true with: Name: ${{ inputs.Name }} @@ -358,7 +358,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -373,7 +373,7 @@ jobs: - name: Test built module id: test - uses: PSModule/Test-PSModule@v2 + uses: ./.github/actions/Test continue-on-error: true with: Name: ${{ inputs.Name }} @@ -411,7 +411,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -548,7 +548,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -601,7 +601,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -714,7 +714,7 @@ jobs: uses: PSModule/Debug@v0 - name: Initialize environment - uses: PSModule/Initialize-PSModule@v1 + uses: ./.github/actions/Initialize with: Debug: ${{ inputs.Debug }} Prerelease: ${{ inputs.Prerelease }} @@ -728,7 +728,7 @@ jobs: path: ${{ inputs.ModulesOutputPath }} - name: Publish module - uses: PSModule/Publish-PSModule@v1 + uses: ./.github/actions/Publish with: Name: ${{ inputs.Name }} ModulePath: ${{ inputs.ModulesOutputPath }} diff --git a/README.md b/README.md index b324c6cc..79305955 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,31 @@ -# Process-PSModule +# PSModule CI/CD Workflow -A workflow for the PSModule process, stitching together the `Initialize`, `Build`, `Test`, and `Publish` actions to create a complete -CI/CD pipeline for PowerShell modules. The workflow is used by all PowerShell modules in the PSModule organization. +## Overview -## Specifications and practices +The **Process-PSModule** workflow is a comprehensive GitHub reusable workflow that automates the **CI/CD pipeline** for PowerShell modules. It integrates the entire module lifecycle, from initialization to publication, ensuring best practices, compatibility, and compliance with industry standards. -Process-PSModule follows: +## Features -- [Test-Driven Development](https://testdriven.io/test-driven-development/) using [Pester](https://pester.dev) and [PSScriptAnalyzer](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/overview?view=ps-modules) -- [GitHub Flow specifications](https://docs.github.com/en/get-started/using-github/github-flow) -- [SemVer 2.0.0 specifications](https://semver.org) -- [Continiuous Delivery practices](https://en.wikipedia.org/wiki/Continuous_delivery) +This workflow seamlessly stitches together the following actions: -## How it works +- **[Initialize-PSModule](https://github.com/PSModule/Initialize-PSModule)**: Prepares the GitHub Actions runner with required dependencies. +- **[Build-PSModule](https://github.com/PSModule/Build-PSModule)**: Compiles the source code into a production-ready PowerShell module. +- **[Test-PSModule](https://github.com/PSModule/Test-PSModule)**: Executes Pester and PSScriptAnalyzer tests to validate the module. +- **[Publish-PSModule](https://github.com/PSModule/Publish-PSModule)**: Publishes the module to the PowerShell Gallery, generates documentation, and creates GitHub releases. -The workflow is designed to be trigger on pull requests to the repository's default branch. -When a pull request is opened, closed, reopened, synchronized (push), or labeled, the workflow will run. -Depending on the labels in the pull requests, the workflow will result in different outcomes. +## How It Works -![Process diagram](./media/Process-PSModule.png) +The workflow triggers automatically on pull requests to the repository's default branch, responding to events such as opening, closing, synchronization, and labeling. Based on the assigned labels, it determines the appropriate workflow actions to execute. -- [Test-PSModule](https://github.com/PSModule/Test-PSModule/) - Tests the source code using PSScriptAnalyzer, PSModule source code tests suites. This runs on 4 different environments to check compatibility. - - PowerShell 7.x on Windows, Ubuntu and macOS. - - Windows PowerShell 5.1 on Windows. -- [Build-PSModule](https://github.com/PSModule/Build-PSModule/) - Compiles the repository into an efficient PowerShell module. -- [Test-PSModule](https://github.com/PSModule/Test-PSModule/) - Tests the compiled module using PSScriptAnalyzer, PSModule module tests and custom module tests from the module repository. This runs on 4 different environments to check compatibility. - - PowerShell 7.x on Windows, Ubuntu and macOS. - - Windows PowerShell 5.1 on Windows. -- [Publish-PSModule](https://github.com/PSModule/Publish-PSModule/) - Publishes the module to the PowerShell Gallery, docs to GitHub Pages, and creates a release on the GitHub repository. +### Key Practices Followed +- **Test-Driven Development** using [Pester](https://pester.dev) and [PSScriptAnalyzer](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/overview?view=ps-modules) +- **GitHub Flow** for streamlined branching and merging +- **Semantic Versioning (SemVer 2.0.0)** to manage module versions systematically +- **Continuous Delivery (CD)** principles for automated deployments -To use the workflow, create a new file in the `.github/workflows` directory of the module repository and add the following content. -
-Workflow suggestion +## Usage + +To use this workflow in your PowerShell module repository, create a new GitHub Actions workflow file under `.github/workflows`: ```yaml name: Process-PSModule @@ -47,89 +41,12 @@ on: - synchronize - labeled -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: write - pull-requests: write - jobs: Process-PSModule: uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v2 secrets: inherit - ``` -
- -## Usage - -### Inputs - -| Name | Type | Description | Required | Default | -| ---- | ---- | ----------- | -------- | ------- | -| `Name` | `string` | The name of the module to process. This defaults to the repository name if nothing is specified. | `false` | N/A | -| `Path` | `string` | The path to the source code of the module. | `false` | `src` | -| `ModulesOutputPath` | `string` | The path to the output directory for the modules. | `false` | `outputs/modules` | -| `DocsOutputPath` | `string` | The path to the output directory for the documentation. | `false` | `outputs/docs` | -| `PublishDocs` | `boolean` | Whether to publish the documentation using MkDocs and GitHub Pages. | `false` | `true` | -| `SiteOutputPath` | `string` | The path to the output directory for the site. | `false` | `outputs/site` | -| `SkipTests` | `string` | Defines what types of tests to skip. Allowed values are 'All', 'SourceCode', 'Module', 'None', 'macOS', 'Windows', 'Linux'. | `false` | `None` | -| `TestProcess` | `boolean` | Whether to test the process. | `false` | `false` | -| `Version` | `string` | Specifies the version of the GitHub module to be installed. The value must be an exact version. | `false` | N/A | -| `Prerelease` | `boolean` | Whether to use a prerelease version of the 'GitHub' module. | `false` | `false` | -| `Debug` | `boolean` | Whether to enable debug output. Adds a `debug` step to every job. | `false` | `false` | -| `Verbose` | `boolean` | Whether to enable verbose output. | `false` | `false` | - -### Secrets - -The following secrets are used by the workflow. They can be automatically provided (if available) by setting the `secrets: inherit` -in the workflow file. - -| Name | Location | Description | Default | -| ---- | -------- | ----------- | ------- | -| `GITHUB_TOKEN` | `github` context | The token used to authenticate with GitHub. | `${{ secrets.GITHUB_TOKEN }}` | -| `APIKey` | GitHub secrets | The API key for the PowerShell Gallery. | N/A | -| `TEST_APP_ENT_CLIENT_ID` | GitHub secrets | The client ID of an Enterprise GitHub App for running tests. | N/A | -| `TEST_APP_ENT_PRIVATE_KEY` | GitHub secrets | The private key of an Enterprise GitHub App for running tests. | N/A | -| `TEST_APP_ORG_CLIENT_ID` | GitHub secrets | The client ID of an Organization GitHub App for running tests. | N/A | -| `TEST_APP_ORG_PRIVATE_KEY` | GitHub secrets | The private key of an Organization GitHub App for running tests. | N/A | -| `TEST_USER_ORG_FG_PAT` | GitHub secrets | The fine-grained personal access token with org access for running tests. | N/A | -| `TEST_USER_USER_FG_PAT` | GitHub secrets | The fine-grained personal access token with user account access for running tests. | N/A | -| `TEST_USER_PAT` | GitHub secrets | The classic personal access token for running tests. | N/A | - -## Permissions - -The action requires the following permissions: - -If running the action in a restrictive mode, the following permissions needs to be granted to the action: - -```yaml -permissions: - contents: write # Create releases - pull-requests: write # Create comments on the PRs - statuses: write # Update the status of the PRs from the linter -``` - -### Publishing to GitHub Pages - -To publish the documentation to GitHub Pages, the action requires the following permissions: - -```yaml -permissions: - pages: write # Deploy to Pages - id-token: write # Verify the deployment originates from an appropriate source -``` - -For more info see [Deploy GitHub Pages site](https://github.com/marketplace/actions/deploy-github-pages-site). - -## Compatibility -The action is compatible with the following configurations: +## Documentation -| OS | Shell | -| --- | --- | -| windows-latest | pwsh | -| macos-latest | pwsh | -| ubuntu-latest | pwsh | +For a detailed guide on how to configure and use the **Process-PSModule** workflow, please review the [documentation](https://PSModule.io/Process-PSModule). diff --git a/docs/Build.md b/docs/Build.md new file mode 100644 index 00000000..54a1e6b9 --- /dev/null +++ b/docs/Build.md @@ -0,0 +1,182 @@ +# Build-PSModule + +This action "compiles" the module source code into an efficient PowerShell module that is ready to be published to the PowerShell Gallery. + +This GitHub Action is a part of the [PSModule framework](https://github.com/PSModule). It is recommended to use the [Process-PSModule workflow](https://github.com/PSModule/Process-PSModule) to automate the whole process of managing the PowerShell module. + +## Supported module types + +- Script module type +- Manifest module type + +## Supported practices and principles + +- [PowerShellGallery Publishing Guidelines and Best Practices](https://learn.microsoft.com/powershell/gallery/concepts/publishing-guidelines) are followed as much as possible. + +## How it works + +During the build process the following steps are performed: + +1. **Runs local build scripts:** Searches for any `*build.ps1` files anywhere in the repository. These scripts are executed in **alphabetical order by filename** (irrespective of their path). +This step lets you add custom build logic to process or modify the module contents before further build steps are performed. +1. **Copies the source code** of the module to an output folder. +1. **Builds the module manifest file** based on information from the GitHub repository and the source code. For more information, please read the [Module Manifest](#module-manifest) section. +1. **Builds the root module (.psm1) file** by combining source code and adding automation into the root module file. For more information, please read the [Root module](#root-module) section. +1. **Builds the module documentation** using platyPS and comment-based help in the source code. For more information, please read the [Module documentation](#module-documentation) section. + +## Usage + +| Name | Description | Required | Default | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------- | +| `Name` | Name of the module to process. | `false` | | +| `Path` | Path to the folder where the modules are located. | `false` | `src` | +| `ModulesOutputPath` | Path to the folder where the built modules are outputted. | `false` | `outputs/modules` | +| `DocsOutputPath` | Path to the folder where the built docs are outputted. | `false` | `outputs/docs` | +| `ModuleArtifactName` | Name of the module artifact to upload. | `false` | `module` | +| `DocsArtifactName` | Name of the docs artifact to upload. | `false` | `docs` | +| `Debug` | Enable debug output. | `false` | `'false'` | +| `Verbose` | Enable verbose output. | `false` | `'false'` | +| `Version` | Specifies the version of the GitHub module to be installed. The value must be an exact version. | `false` | | +| `Prerelease` | Allow prerelease versions if available. | `false` | `'false'` | + +## Root module + +The `src` folder may contain a 'root module' file. If present, the build function will disregard this file and build a new root module file based on the source code in the module folder. + +The root module file is the main file that is loaded when the module is imported. It is built from the source code files in the module folder in the following order: + +1. Adds a module header from `header.ps1` if it exists and removes the file from the module folder. +1. Adds a data loader that loads files from the `data` folder as variables in the module scope, if the folder exists. The variables are available using the `$script:` syntax. +1. Adds content from the following folders into the root module file. The files on the root of a folder are added before recursively processing subfolders (folders are processed in alphabetical order). Once a file is processed, it is removed from the module folder. + 1. `init` + 1. `classes/private` + 1. `classes/public` + 1. `functions/private` + 1. `functions/public` + 1. `variables/private` + 1. `variables/public` + 1. `*.ps1` on module root +1. Adds a `class` and `enum` exporter that exports the ones from the `classes/public` folder to the caller session, using [TypeAccelerators](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-7.4#exporting-classes-with-type-accelerators). +1. Adds the `Export-ModuleMember` function to the end of the file, to ensure that only the functions, cmdlets, variables and aliases defined in the `public` folders are exported. + +## Module manifest + +The module manifest file describes the module and its contents. PowerShell uses it to load the module and its prerequisites. It also contains important metadata used by the PowerShell Gallery. If a file exists in the source code folder (`src`), it will be used as the base for the module manifest file. While most values in the module manifest are calculated during the build process, some values are preserved if specified in the source manifest file. + +During the module manifest build process the following steps are performed: + +1. Get the manifest file from the source code. If it does not exist, a new manifest file is created. +1. Generate and set the `RootModule` based on the module name. +1. Set a temporary `ModuleVersion` (this is updated during the release process by [Publish-PSModule](https://github.com/PSModule/Publish-PSModule)). +1. Set the `Author` and `CompanyName` based on the GitHub Owner. If a value exists in the source manifest file, that value is used. +1. Set the `Copyright` information based on a default text (`(c) 2024 >>OwnerName<<. All rights reserved.`) and includes the `Author`, `CompanyName` or both when applicable. If a value exists in the source manifest file, that value is used. +1. Set the `Description` based on the GitHub repository description. If a value exists in the source manifest file, that value is used. +1. Set various properties such as `PowerShellHostName`, `PowerShellHostVersion`, `DotNetFrameworkVersion`, `ClrVersion`, and `ProcessorArchitecture`. If values exist in the source manifest file, those values are used. +1. Get the list of files in the module source folder and set the `FileList` property in the manifest. +1. Get the list of required assemblies (`*.dll` files) from the `assemblies` and `modules` (depth = 1) folder and set the `RequiredAssemblies` property. +1. Get the list of nested modules (`*.psm1`, `*.ps1` and `*.dll` files one level down) from the `modules` folder and set the `NestedModules` property. +1. Get the list of scripts to process (`*.ps1` files) from the `scripts` folder and set the `ScriptsToProcess` property. This ensures that the scripts are loaded into the caller session. +1. Get the list of types to process by searching for `*.Types.ps1xml` files in the entire module source folder and set the `TypesToProcess` property. +1. Get the list of formats to process by searching for `*.Format.ps1xml` files in the entire module source folder and set the `FormatsToProcess` property. +1. Get the list of DSC resources to export by searching for `*.psm1` files in the `resources` folder and set the `DscResourcesToExport` property. +1. Get the list of functions, cmdlets, aliases, and variables from the respective `\public` folders and set the respective properties in the manifest. +1. Get the list of modules by searching for all `*.psm1` files in the entire module source folder (excluding the root module) and set the `ModuleList` property. +1. Gather information from source files to update `RequiredModules`, `PowerShellVersion`, and `CompatiblePSEditions` properties. +1. Gather additional information from the GitHub repository: + - `Tags` are generated from repository topics plus compatibility tags from the source files. + - `LicenseUri` is generated assuming there is a `LICENSE` file at the repository root. If a value exists in the source manifest file, that value is used. + - `ProjectUri` is set to the GitHub repository URL. If a value exists in the source manifest file, that value is used. + - `IconUri` is generated assuming there is an `icon.png` file in the `icon` folder at the repository root. If a value exists in the source manifest file, that value is used. +1. `ReleaseNotes` are not automated (could be set via PR or release description). +1. `PreRelease` is managed externally by [Publish-PSModule](https://github.com/PSModule/Publish-PSModule). +1. `RequireLicenseAcceptance` defaults to `false` unless specified in the source manifest. +1. `ExternalModuleDependencies` is not automated. If specified in the source manifest, that value is used. +1. `HelpInfoURI` is not automated. If specified in the source manifest, that value is used. +1. Create a new manifest file in the output folder with the gathered information, which also generates a new `GUID` for the module. +1. Format the manifest file using the `Set-ModuleManifest` function from the [Utilities](https://github.com/PSModule/Utilities) module. + +Linking the description to the module manifest file might show more how this works: + +```powershell +@{ + RootModule = 'Utilities.psm1' # Generated from the module name, .psm1 + ModuleVersion = '0.0.1' # Set during release using Publish-PSModule. + CompatiblePSEditions = @() # Get from source files, requires -PSEdition , null if not provided. + GUID = '' # Generated when saving the manifest using New-ModuleManifest. + Author = 'PSModule' # Derived from the GitHub Owner, unless specified in the source manifest. + CompanyName = 'PSModule' # Derived from the GitHub Owner, unless specified in the source manifest. + Copyright = '(c) 2024 PSModule. All rights reserved.' + Description = 'This is a module.' # Taken from the repository description or the source manifest. + PowerShellVersion = '' # Derived from source files, requires -Version [.], null if not provided. + PowerShellHostName = '' # Taken from the manifest file, null if not provided. + PowerShellHostVersion = '' # Taken from the manifest file, null if not provided. + DotNetFrameworkVersion = '' # Taken from the manifest file, null if not provided. + ClrVersion = '' # Taken from the manifest file, null if not provided. + ProcessorArchitecture = '' # Taken from the manifest file, null if not provided. + RequiredModules = @() # Derived from source files, ensuring required modules are installed. + RequiredAssemblies = @() # Collected from assemblies\*.dll and modules\*.dll. + ScriptsToProcess = @() # Collected from scripts\*.ps1 and classes\*.ps1 ordered by name. + TypesToProcess = @() # Collected from *.Types.ps1xml files in the source folder. + FormatsToProcess = @() # Collected from *.Format.ps1xml files in the source folder. + NestedModules = @() # Collected from modules\*.psm1, modules\*.ps1, and modules\*.dll. + FunctionsToExport = @() # Collected from public\*.ps1 files. + CmdletsToExport = @() # Taken from the manifest file, or empty if not provided. + VariablesToExport = @() # Collected from variables\public\*.ps1. + AliasesToExport = '*' # Derived from functions\public\*.ps1. + DscResourcesToExport = @() # Collected from resources\*.psm1. + ModuleList = @() # A listing of all .\*.psm1 files (informational only). + FileList = @() # A listing of all .\* files (informational only). + PrivateData = @{ + PSData = @{ + Tags = @() # Derived from repository topics and compatibility tags. + LicenseUri = '' # Generated public link to .\LICENSE. + ProjectUri = '' # Generated public link to the GitHub Repository. + IconUri = '' # Derived from .\icon\icon.png. + ReleaseNotes = '' # To be updated during release. + Prerelease = '' # Normalized version of the branch name, updated during release. + RequireLicenseAcceptance = $false + ExternalModuleDependencies = @() + ExperimentalFeatures = @( + @{ + Name = "SomeExperimentalFeature" + Description = "This is an experimental feature." + } + ) + } + OtherKeys = @{} + } + HelpInfoURI = '' # Taken from the source manifest if provided. + DefaultCommandPrefix = '' # Taken from the source manifest if provided. +} +``` + +## Module documentation + +The module documentation is built using `platyPS` and comment-based help in the source code. The documentation is currently not published anywhere but is expected to be published to GitHub Pages in a future release. + +## Permissions + +This action does not require any special permissions. + +## Sources + +**Module manifest:** + +- [about_Module_Manifests](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_module_manifests) +- [How to write a PowerShell module manifest](https://learn.microsoft.com/powershell/scripting/developer/module/how-to-write-a-powershell-module-manifest) +- [New-ModuleManifest](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/new-modulemanifest) +- [Update-ModuleManifest](https://learn.microsoft.com/powershell/module/powershellget/update-modulemanifest) +- [Package metadata values that impact the PowerShell Gallery UI](https://learn.microsoft.com/powershell/gallery/concepts/package-manifest-affecting-ui#powershell-gallery-feature-elements-controlled-by-the-module-manifest) +- [PowerShellGallery Publishing Guidelines and Best Practices](https://learn.microsoft.com/en-us/powershell/gallery/concepts/publishing-guidelines#tag-your-package-with-the-compatible-pseditions-and-platforms) + +**Modules:** + +- [PowerShell scripting performance considerations](https://learn.microsoft.com/powershell/scripting/dev-cross-plat/performance/script-authoring-considerations) +- [PowerShell module authoring considerations](https://learn.microsoft.com/powershell/scripting/dev-cross-plat/performance/module-authoring-considerations) + +**Documentation:** + +- [platyPS reference](https://learn.microsoft.com/powershell/module/platyps/?source=recommendations) +- [PlatyPS overview](https://learn.microsoft.com/powershell/utility-modules/platyps/overview?view=ps-modules) +- [about_Comment_Based_Help](https://go.microsoft.com/fwlink/?LinkID=123415) +- [Supporting Updatable Help](https://learn.microsoft.com/powershell/scripting/developer/help/supporting-updatable-help) diff --git a/docs/Document.md b/docs/Document.md new file mode 100644 index 00000000..d560186e --- /dev/null +++ b/docs/Document.md @@ -0,0 +1,17 @@ +# Template-Action + +A template repository for GitHub Actions + +## Usage + +### Inputs + +### Secrets + +### Outputs + +### Example + +```yaml +Example here +``` diff --git a/docs/Initialize.md b/docs/Initialize.md new file mode 100644 index 00000000..c59e0c3b --- /dev/null +++ b/docs/Initialize.md @@ -0,0 +1,77 @@ +# Initialize-PSModule + +An action that is used to prepare the runner for PSModule framework. + +This GitHub Action is a part of the [PSModule framework](https://github.com/PSModule). It is recommended to use the [Process-PSModule workflow](https://github.com/PSModule/Process-PSModule) to automate the whole process of managing the PowerShell module. + +## Specifications and practices + +Initiate-PSModule follows: + +- [SemVer 2.0.0 specifications](https://semver.org) +- [GitHub Flow specifications](https://docs.github.com/en/get-started/using-github/github-flow) +- [Continiuous Delivery practices](https://en.wikipedia.org/wiki/Continuous_delivery) + +... and supports the following practices in the PSModule framework: + +- [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) + +## How it works + +The Initialize-PSModule action will prepare the runner for the PSModule framework by installing the following dependencies: + +| Module | Description | +| --- | --- | +| GitHub | Used to interact with the GitHub API and GitHub Action workflow commands. | +| PSScriptAnalyzer | Used to lint and format PowerShell code. | +| PSSemVer | Used to create an object for the semantic version numbers. Has functionality to compare, and bump versions. | +| Pester | Used for testing PowerShell code. | +| Utilities | Used by all actions, contains common function and classes. | +| platyPS | Used to generate Markdown documentation from PowerShell code. | +| powershell-yaml | Used to parse and serialize YAML files, typically for reading configuration files. | + +## Usage + +### Inputs + +| Name | Description | Required | Default | +| - | - | - | - | +| `Debug` | Enable debug output. | `false` | `'false'` | +| `Verbose` | Enable verbose output. | `false` | `'false'` | +| `Version` | Specifies the version of the GitHub module to be installed. The value must be an exact version. | `false` | | +| `Prerelease` | Allow prerelease versions if available. | `false` | `'false'` | + +## Example + +The action can be used in a workflow to prepare the runner for the PSModule framework by adding it at the start of the workflow. + +```yaml +name: Process-PSModule + +on: [push] + +jobs: + Process-PSModule: + name: Process module + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize +``` + +## Permissions + +The action does not require any permissions. + +## Compatibility + +The action is compatible with the following configurations: + +| OS | Shell | +| --- | --- | +| windows-latest | pwsh | +| macos-latest | pwsh | +| ubuntu-latest | pwsh | diff --git a/docs/Publish.md b/docs/Publish.md new file mode 100644 index 00000000..398db254 --- /dev/null +++ b/docs/Publish.md @@ -0,0 +1,119 @@ +# Publish-PSModule + +Creates a GitHub release and publishes the PowerShell module to the PowerShell Gallery. + +This GitHub Action is a part of the [PSModule framework](https://github.com/PSModule). It is recommended to use the [Process-PSModule workflow](https://github.com/PSModule/Process-PSModule) to automate the whole process of managing the PowerShell module. + +## Specifications and practices + +Publish-PSModule follows: + +- [SemVer 2.0.0 specifications](https://semver.org) +- [GitHub Flow specifications](https://docs.github.com/en/get-started/using-github/github-flow) +- [Continiuous Delivery practices](https://en.wikipedia.org/wiki/Continuous_delivery) + +... and supports the following practices in the PSModule framework: + +- [PowerShell publishing guidelines](https://learn.microsoft.com/en-us/powershell/gallery/concepts/publishing-guidelines?view=powershellget-3.x) + +## How it works + +The workflow will trigger on pull requests to the repositorys default branch. +When the pull request is opened, the action will decide what to do based on labels on the pull request. + +It will get the latest release version by looking up the versions in GitHub releases, PowerShell Gallery and the module manifest. +The next version is then determined by the labels on the pull request. If a prerelease label is found, the action will create a +prerelease with the branch name (in normalized form) as the prerelease name. By defualt, the following labels are used: + +- For a major release, and increasing the first number in the version use: + - `major` + - `breaking` +- For a minor release, and increasing the second number in the version. + - `minor` + - `feature` +- For a patch release, and increases the third number in the version. + - `patch` + - `fix` + +The types of labels used for the types of prereleases can be configured using the `MajorLabels`, `MinorLabels` and `PatchLabels` +parameters/settings in the configuration file. See the [Usage](#usage) section for more information. + +When a pull request is merged into the default branch, the action will create a release based on the labels and clean up any previous +prereleases that was created. + +## Usage + +The action can be configured using the following settings: + +| Name | Description | Required | Default | +| --- | --- | --- | --- | +| `APIKey` | PowerShell Gallery API Key. | `true` | | +| `AutoCleanup`| Control wether to automatically cleanup prereleases. If disabled, the action will not remove any prereleases. | `false` | `true` | +| `AutoPatching` | Control wether to automatically handle patches. If disabled, the action will only create a patch release if the pull request has a 'patch' label. | `false` | `true` | +| `ConfigurationFile` | The path to the configuration file. Settings in the configuration file take precedence over the action inputs. | `false` | `.github\auto-release.yml` | +| `DatePrereleaseFormat` | The format to use for the prerelease number using [.NET DateTime format strings](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings). | `false` | `''` | +| `IgnoreLabels` | A comma separated list of labels that do not trigger a release. | `false` | `NoRelease` | +| `IncrementalPrerelease` | Control wether to automatically increment the prerelease number. If disabled, the action will ensure only one prerelease exists for a given branch. | `false` | `true` | +| `MajorLabels` | A comma separated list of labels that trigger a major release. | `false` | `major, breaking` | +| `MinorLabels` | A comma separated list of labels that trigger a minor release. | `false` | `minor, feature` | +| `ModulePath` | Path to the folder where the module to publish is located. | `false` | `outputs/modules` | +| `Name` | Name of the module to publish. Defaults to the repository name. | `false` | | +| `PatchLabels` | A comma separated list of labels that trigger a patch release. | `false` | `patch, fix` | +| `VersionPrefix` | The prefix to use for the version number. | `false` | `v` | +| `WhatIf` | Control wether to simulate the action. If enabled, the action will not create any releases. Used for testing. | `false` | `false` | +| `Debug` | Enable debug output. | `'false'` | `false` | +| `Verbose` | Enable verbose output. | `'false'` | `false` | +| `Version` | Specifies the version of the GitHub module to be installed. The value must be an exact version. | | `false` | +| `Prerelease` | Allow prerelease versions if available. | `'false'` | `false` | + +### Configuration file + +The configuration file is a YAML file that can be used to configure the action. +By default, the configuration file is expected at `.github\auto-release.yml`, which can be changed using the `ConfigurationFile` setting. +The actions configuration can be change by altering the settings in the configuration file. + +```yaml +DatePrereleaseFormat: 'yyyyMMddHHmm' +IncrementalPrerelease: false +VersionPrefix: '' +``` + +This example uses the date format for the prerelease, disables the incremental prerelease and removes the version prefix. + +## Example + +```yaml +name: Publish-PSModule + +on: [pull_request] + +jobs: + Publish-PSModule: + name: Publish-PSModule + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize + + - name: Publish-PSModule + uses: ./.github/actions/Publish + env: + GITHUB_TOKEN: ${{ github.token }} + with: + APIKey: ${{ secrets.APIKEY }} +``` + +## Permissions + +The action requires the following permissions: + +If running the action in a restrictive mode, the following permissions needs to be granted to the action: + +```yaml +permissions: + contents: write # Required to create releases + pull-requests: write # Required to create comments on the PRs +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..b324c6cc --- /dev/null +++ b/docs/README.md @@ -0,0 +1,135 @@ +# Process-PSModule + +A workflow for the PSModule process, stitching together the `Initialize`, `Build`, `Test`, and `Publish` actions to create a complete +CI/CD pipeline for PowerShell modules. The workflow is used by all PowerShell modules in the PSModule organization. + +## Specifications and practices + +Process-PSModule follows: + +- [Test-Driven Development](https://testdriven.io/test-driven-development/) using [Pester](https://pester.dev) and [PSScriptAnalyzer](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/overview?view=ps-modules) +- [GitHub Flow specifications](https://docs.github.com/en/get-started/using-github/github-flow) +- [SemVer 2.0.0 specifications](https://semver.org) +- [Continiuous Delivery practices](https://en.wikipedia.org/wiki/Continuous_delivery) + +## How it works + +The workflow is designed to be trigger on pull requests to the repository's default branch. +When a pull request is opened, closed, reopened, synchronized (push), or labeled, the workflow will run. +Depending on the labels in the pull requests, the workflow will result in different outcomes. + +![Process diagram](./media/Process-PSModule.png) + +- [Test-PSModule](https://github.com/PSModule/Test-PSModule/) - Tests the source code using PSScriptAnalyzer, PSModule source code tests suites. This runs on 4 different environments to check compatibility. + - PowerShell 7.x on Windows, Ubuntu and macOS. + - Windows PowerShell 5.1 on Windows. +- [Build-PSModule](https://github.com/PSModule/Build-PSModule/) - Compiles the repository into an efficient PowerShell module. +- [Test-PSModule](https://github.com/PSModule/Test-PSModule/) - Tests the compiled module using PSScriptAnalyzer, PSModule module tests and custom module tests from the module repository. This runs on 4 different environments to check compatibility. + - PowerShell 7.x on Windows, Ubuntu and macOS. + - Windows PowerShell 5.1 on Windows. +- [Publish-PSModule](https://github.com/PSModule/Publish-PSModule/) - Publishes the module to the PowerShell Gallery, docs to GitHub Pages, and creates a release on the GitHub repository. + +To use the workflow, create a new file in the `.github/workflows` directory of the module repository and add the following content. +
+Workflow suggestion + +```yaml +name: Process-PSModule + +on: + pull_request: + branches: + - main + types: + - closed + - opened + - reopened + - synchronize + - labeled + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + Process-PSModule: + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@v2 + secrets: inherit + +``` +
+ +## Usage + +### Inputs + +| Name | Type | Description | Required | Default | +| ---- | ---- | ----------- | -------- | ------- | +| `Name` | `string` | The name of the module to process. This defaults to the repository name if nothing is specified. | `false` | N/A | +| `Path` | `string` | The path to the source code of the module. | `false` | `src` | +| `ModulesOutputPath` | `string` | The path to the output directory for the modules. | `false` | `outputs/modules` | +| `DocsOutputPath` | `string` | The path to the output directory for the documentation. | `false` | `outputs/docs` | +| `PublishDocs` | `boolean` | Whether to publish the documentation using MkDocs and GitHub Pages. | `false` | `true` | +| `SiteOutputPath` | `string` | The path to the output directory for the site. | `false` | `outputs/site` | +| `SkipTests` | `string` | Defines what types of tests to skip. Allowed values are 'All', 'SourceCode', 'Module', 'None', 'macOS', 'Windows', 'Linux'. | `false` | `None` | +| `TestProcess` | `boolean` | Whether to test the process. | `false` | `false` | +| `Version` | `string` | Specifies the version of the GitHub module to be installed. The value must be an exact version. | `false` | N/A | +| `Prerelease` | `boolean` | Whether to use a prerelease version of the 'GitHub' module. | `false` | `false` | +| `Debug` | `boolean` | Whether to enable debug output. Adds a `debug` step to every job. | `false` | `false` | +| `Verbose` | `boolean` | Whether to enable verbose output. | `false` | `false` | + +### Secrets + +The following secrets are used by the workflow. They can be automatically provided (if available) by setting the `secrets: inherit` +in the workflow file. + +| Name | Location | Description | Default | +| ---- | -------- | ----------- | ------- | +| `GITHUB_TOKEN` | `github` context | The token used to authenticate with GitHub. | `${{ secrets.GITHUB_TOKEN }}` | +| `APIKey` | GitHub secrets | The API key for the PowerShell Gallery. | N/A | +| `TEST_APP_ENT_CLIENT_ID` | GitHub secrets | The client ID of an Enterprise GitHub App for running tests. | N/A | +| `TEST_APP_ENT_PRIVATE_KEY` | GitHub secrets | The private key of an Enterprise GitHub App for running tests. | N/A | +| `TEST_APP_ORG_CLIENT_ID` | GitHub secrets | The client ID of an Organization GitHub App for running tests. | N/A | +| `TEST_APP_ORG_PRIVATE_KEY` | GitHub secrets | The private key of an Organization GitHub App for running tests. | N/A | +| `TEST_USER_ORG_FG_PAT` | GitHub secrets | The fine-grained personal access token with org access for running tests. | N/A | +| `TEST_USER_USER_FG_PAT` | GitHub secrets | The fine-grained personal access token with user account access for running tests. | N/A | +| `TEST_USER_PAT` | GitHub secrets | The classic personal access token for running tests. | N/A | + +## Permissions + +The action requires the following permissions: + +If running the action in a restrictive mode, the following permissions needs to be granted to the action: + +```yaml +permissions: + contents: write # Create releases + pull-requests: write # Create comments on the PRs + statuses: write # Update the status of the PRs from the linter +``` + +### Publishing to GitHub Pages + +To publish the documentation to GitHub Pages, the action requires the following permissions: + +```yaml +permissions: + pages: write # Deploy to Pages + id-token: write # Verify the deployment originates from an appropriate source +``` + +For more info see [Deploy GitHub Pages site](https://github.com/marketplace/actions/deploy-github-pages-site). + +## Compatibility + +The action is compatible with the following configurations: + +| OS | Shell | +| --- | --- | +| windows-latest | pwsh | +| macos-latest | pwsh | +| ubuntu-latest | pwsh | diff --git a/docs/Test.md b/docs/Test.md new file mode 100644 index 00000000..44fdafaa --- /dev/null +++ b/docs/Test.md @@ -0,0 +1,129 @@ +# Test-PSModule + +Test PowerShell modules with Pester and PSScriptAnalyzer. + +This GitHub Action is a part of the [PSModule framework](https://github.com/PSModule). It is recommended to use the [Process-PSModule workflow](https://github.com/PSModule/Process-PSModule) to automate the whole process of managing the PowerShell module. + +## Specifications and practices + +Test-PSModule enables: + +- [Test-Driven Development](https://testdriven.io/test-driven-development/) using [Pester](https://pester.dev) and [PSScriptAnalyzer](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/overview?view=ps-modules) + +## How it works + +The action runs the following the Pester test framework: +- [PSScriptAnalyzer tests](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/readme?view=ps-modules) +- [PSModule framework tests](#psmodule-tests) +- If `TestType` is set to `Module`: + - The module manifest is tested using [Test-ModuleManifest](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/test-modulemanifest). + - The module is imported. + - Custom module tests from the `tests` directory in the module repository are run. + - CodeCoverage is calculated. +- If `TestType` is set to `SourceCode`: + - The source code is tested with: + - `PSScriptAnalyzer` for best practices, using custom settings. + - `PSModule.SourceCode` for other PSModule standards. +- The action returns a `passed` output that is `true` if all tests pass, else `false`. +- The following reports are calculated and uploaded as artifacts: + - Test suite results. + - Code coverage results. + +The action fails if any of the tests fail or it fails to run the tests. +This is mitigated by the `continue-on-error` option in the workflow. + +## How to use it + +To use the action, create a new file in the `.github/workflows` directory of the module repository and add the following content. +
+Workflow suggestion - before module is built + +```yaml +name: Test-PSModule + +on: [push] + +jobs: + Test-PSModule: + name: Test-PSModule + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize + + - name: Test-PSModule + uses: ./.github/actions/Test + with: + Path: src + TestType: SourceCode + +``` +
+ +
+Workflow suggestion - after module is built + +```yaml +name: Test-PSModule + +on: [push] + +jobs: + Test-PSModule: + name: Test-PSModule + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Initialize environment + uses: ./.github/actions/Initialize + + - name: Test-PSModule + uses: PSModule/Test-PSModule@main + with: + Path: outputs/modules + TestType: Module + +``` +
+ +## Usage + +### Inputs + +| Name | Description | Required | Default | +| ---- | ----------- | -------- | ------- | +| `Path` | The path to the code to test. | `true` | | +| `TestType` | The type of tests to run. Can be either `Module` or `SourceCode`. | `true` | | +| `Name` | The name of the module to test. The name of the repository is used if not specified. | `false` | | +| `TestsPath` | The path to the tests to run. | `false` | `tests` | +| `StackTraceVerbosity` | Verbosity level of the stack trace. Allowed values: `None`, `FirstLine`, `Filtered`, `Full`. | `false` | `Filtered` | +| `Verbosity` | Verbosity level of the test output. Allowed values: `None`, `Normal`, `Detailed`, `Diagnostic`. | `false` | `Detailed` | +| `Debug` | Enable debug output. | `'false'` | `false` | +| `Verbose` | Enable verbose output. | `'false'` | `false` | +| `Version` | Specifies the version of the GitHub module to be installed. The value must be an exact version. | | `false` | +| `Prerelease` | Allow prerelease versions if available. | `'false'` | `false` | + +### Outputs + +| Name | Description | Possible values | +| ---- | ----------- | --------------- | +| `passed` | If the tests passed. | `true`, `false` | + +## PSModule tests + +The [PSModule framework tests](https://github.com/PSModule/Test-PSModule/blob/main/scripts/tests/PSModule/PSModule.Tests.ps1) verifies the following coding practices that the framework enforces: + +- Script filename and function/filter name should match. + +## Tools + +- Pester | [Docs](https://www.pester.dev) | [GitHub](https://github.com/Pester/Pester) | [PS Gallery](https://www.powershellgallery.com/packages/Pester/) +- PSScriptAnalyzer [Docs](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/overview?view=ps-modules) | [GitHub](https://github.com/PowerShell/PSScriptAnalyzer) | [PS Gallery](https://www.powershellgallery.com/packages/PSScriptAnalyzer/) +- PSResourceGet | [Docs](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.psresourceget/?view=powershellget-3.x) | [GitHub](https://github.com/PowerShell/PSResourceGet) | [PS Gallery](https://www.powershellgallery.com/packages/Microsoft.PowerShell.PSResourceGet/) +- [Test-ModuleManifest | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/test-modulemanifest) +- [PowerShellGet | Microsoft Learn](https://learn.microsoft.com/en-us/powershell/module/PowerShellGet/test-scriptfileinfo) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..949abbbd --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,85 @@ +site_name: PSModule Docs +theme: + name: material + language: en + font: + text: Roboto + code: Sono + logo: assets/icon.png + favicon: assets/icon.png + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/link + name: Switch to dark mode + # Palette toggle for dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + toggle: + primary: black + accent: green + icon: material/toggle-switch-off-outline + name: Switch to light mode + # Palette toggle for light mode + - media: '(prefers-color-scheme: light)' + scheme: default + toggle: + primary: indigo + accent: green + icon: material/toggle-switch + name: Switch to system preference + icon: + repo: material/github + features: + - navigation.instant + - navigation.instant.progress + - navigation.tabs + - navigation.indexes + # - navigation.top + - navigation.tracking + - search.suggest + - search.highlight + +nav: + - Home: README.md + - Projects: + - PowerShell Modules: PowerShell/index.md + - GitHub Actions: GitHub-Actions/index.md + +repo_name: PSModule/docs +repo_url: https://github.com/PSModule/docs + +plugins: + - search + - git-authors + - git-revision-date-localized: + enable_creation_date: true + type: timeago + +markdown_extensions: + - toc: + permalink: true # Adds a link icon to headings + - attr_list + - admonition + - md_in_html + - pymdownx.details # Enables collapsible admonitions + +extra: + social: + - icon: fontawesome/brands/discord + link: https://discord.gg/jedJWCPAhD + name: PSModule on Discord + - icon: fontawesome/brands/github + link: https://github.com/PSModule/ + name: PSModule on GitHub + consent: + title: Cookie consent + description: >- + We use cookies to recognize your repeated visits and preferences, as well + as to measure the effectiveness of our documentation and whether users + find what they're searching for. With your consent, you're helping us to + make our documentation better. + actions: + - accept + - reject diff --git a/tests/README.md b/tests/outputTestRepo/README.md similarity index 100% rename from tests/README.md rename to tests/outputTestRepo/README.md diff --git a/tests/mkdocs.yml b/tests/outputTestRepo/mkdocs.yml similarity index 100% rename from tests/mkdocs.yml rename to tests/outputTestRepo/mkdocs.yml diff --git a/tests/outputTestRepo/outputs/docs/PSModule/Get-PSModuleTest.md b/tests/outputTestRepo/outputs/docs/PSModule/Get-PSModuleTest.md new file mode 100644 index 00000000..2179b427 --- /dev/null +++ b/tests/outputTestRepo/outputs/docs/PSModule/Get-PSModuleTest.md @@ -0,0 +1,73 @@ +--- +external help file: PSModuleTest-help.xml +Module Name: PSModuleTest +online version: +schema: 2.0.0 +--- + +# Get-PSModuleTest + +## SYNOPSIS +Performs tests on a module. + +## SYNTAX + +```powershell +Get-PSModuleTest [-Name] [-ProgressAction ] [] +``` + +## DESCRIPTION +{{ Fill in the Description }} + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Test-PSModule -Name 'World' +``` + +"Hello, World!" + +## PARAMETERS + +### -Name +Name of the person to greet. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +## RELATED LINKS + diff --git a/tests/outputTestRepo/outputs/docs/PSModule/New-PSModuleTest.md b/tests/outputTestRepo/outputs/docs/PSModule/New-PSModuleTest.md new file mode 100644 index 00000000..68096092 --- /dev/null +++ b/tests/outputTestRepo/outputs/docs/PSModule/New-PSModuleTest.md @@ -0,0 +1,76 @@ +--- +external help file: PSModuleTest-help.xml +Module Name: PSModuleTest +online version: +schema: 2.0.0 +--- + +# New-PSModuleTest + +## SYNOPSIS +Performs tests on a module. + +## SYNTAX + +```powershell +New-PSModuleTest [-Name] [-ProgressAction ] [] +``` + +## DESCRIPTION +{{ Fill in the Description }} + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Test-PSModule -Name 'World' +``` + +"Hello, World!" + +## PARAMETERS + +### -Name +Name of the person to greet. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +Testing if a module can have a [Markdown based link](https://example.com). +!"#¤%&/()=?`´^¨*'-_+§½{[]}<>|@£$€¥¢:;.," +\[This is a test\] + +## RELATED LINKS + diff --git a/tests/src/functions/public/PSModule/PSModule.md b/tests/outputTestRepo/outputs/docs/PSModule/PSModule.md similarity index 100% rename from tests/src/functions/public/PSModule/PSModule.md rename to tests/outputTestRepo/outputs/docs/PSModule/PSModule.md diff --git a/tests/outputTestRepo/outputs/docs/SomethingElse/Set-PSModuleTest.md b/tests/outputTestRepo/outputs/docs/SomethingElse/Set-PSModuleTest.md new file mode 100644 index 00000000..8c7fcf15 --- /dev/null +++ b/tests/outputTestRepo/outputs/docs/SomethingElse/Set-PSModuleTest.md @@ -0,0 +1,73 @@ +--- +external help file: PSModuleTest-help.xml +Module Name: PSModuleTest +online version: +schema: 2.0.0 +--- + +# Set-PSModuleTest + +## SYNOPSIS +Performs tests on a module. + +## SYNTAX + +```powershell +Set-PSModuleTest [-Name] [-ProgressAction ] [] +``` + +## DESCRIPTION +{{ Fill in the Description }} + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Test-PSModule -Name 'World' +``` + +"Hello, World!" + +## PARAMETERS + +### -Name +Name of the person to greet. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +## RELATED LINKS + diff --git a/tests/src/functions/public/SomethingElse/SomethingElse.md b/tests/outputTestRepo/outputs/docs/SomethingElse/SomethingElse.md similarity index 100% rename from tests/src/functions/public/SomethingElse/SomethingElse.md rename to tests/outputTestRepo/outputs/docs/SomethingElse/SomethingElse.md diff --git a/tests/outputTestRepo/outputs/docs/Test-PSModuleTest.md b/tests/outputTestRepo/outputs/docs/Test-PSModuleTest.md new file mode 100644 index 00000000..ce17f7bb --- /dev/null +++ b/tests/outputTestRepo/outputs/docs/Test-PSModuleTest.md @@ -0,0 +1,73 @@ +--- +external help file: PSModuleTest-help.xml +Module Name: PSModuleTest +online version: +schema: 2.0.0 +--- + +# Test-PSModuleTest + +## SYNOPSIS +Performs tests on a module. + +## SYNTAX + +```powershell +Test-PSModuleTest [-Name] [-ProgressAction ] [] +``` + +## DESCRIPTION +{{ Fill in the Description }} + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Test-PSModule -Name 'World' +``` + +"Hello, World!" + +## PARAMETERS + +### -Name +Name of the person to greet. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES + +## RELATED LINKS + diff --git a/tests/outputTestRepo/outputs/modules/PSModuleTest/PSModuleTest.psd1 b/tests/outputTestRepo/outputs/modules/PSModuleTest/PSModuleTest.psd1 new file mode 100644 index 00000000..4dfc77af --- /dev/null +++ b/tests/outputTestRepo/outputs/modules/PSModuleTest/PSModuleTest.psd1 @@ -0,0 +1,92 @@ +@{ + RootModule = 'PSModuleTest.psm1' + ModuleVersion = '999.0.0' + CompatiblePSEditions = @( + 'Core' + 'Desktop' + ) + GUID = 'e4fb58a5-2027-4542-b7be-e5e5f352e358' + Author = 'PSModule' + CompanyName = 'PSModule' + Copyright = '(c) 2025 PSModule. All rights reserved.' + Description = 'Process a module from source code to published module.' + PowerShellVersion = '5.1' + ProcessorArchitecture = 'None' + RequiredModules = @( + @{ + ModuleVersion = '1.1.8' + ModuleName = 'DynamicParams' + } + @{ + RequiredVersion = '1.0.0' + ModuleName = 'PSSemVer' + } + @{ + ModuleVersion = '0.3.1' + ModuleName = 'Store' + } + 'Utilities' + ) + RequiredAssemblies = 'assemblies/LsonLib.dll' + ScriptsToProcess = 'scripts/loader.ps1' + TypesToProcess = @( + 'types/DirectoryInfo.Types.ps1xml' + 'types/FileInfo.Types.ps1xml' + ) + FormatsToProcess = @( + 'formats/CultureInfo.Format.ps1xml' + 'formats/Mygciview.Format.ps1xml' + ) + NestedModules = @( + 'modules/OtherPSModule.psm1' + ) + FunctionsToExport = @( + 'Get-PSModuleTest' + 'New-PSModuleTest' + 'Set-PSModuleTest' + 'Test-PSModuleTest' + ) + CmdletsToExport = @() + VariablesToExport = @( + 'Moons' + 'Planets' + 'SolarSystems' + ) + AliasesToExport = @( + 'New-PSModuleTestAlias1' + 'New-PSModuleTestAlias2' + 'New-PSModuleTestAlias3' + 'New-PSModuleTestAlias4' + 'New-PSModuleTestAlias5' + ) + ModuleList = @( + 'modules/OtherPSModule.psm1' + ) + FileList = @( + 'PSModuleTest.psm1' + 'assemblies/LsonLib.dll' + 'data/Config.psd1' + 'data/Settings.psd1' + 'formats/CultureInfo.Format.ps1xml' + 'formats/Mygciview.Format.ps1xml' + 'modules/OtherPSModule.psm1' + 'scripts/loader.ps1' + 'types/DirectoryInfo.Types.ps1xml' + 'types/FileInfo.Types.ps1xml' + ) + PrivateData = @{ + PSData = @{ + Tags = @( + 'workflow' + 'powershell' + 'powershell-module' + 'PSEdition_Desktop' + 'PSEdition_Core' + ) + LicenseUri = 'https://github.com/PSModule/Process-PSModule/blob/main/LICENSE' + ProjectUri = 'https://github.com/PSModule/Process-PSModule' + IconUri = 'https://raw.githubusercontent.com/PSModule/Process-PSModule/main/icon/icon.png' + } + } +} + diff --git a/tests/outputTestRepo/outputs/modules/PSModuleTest/PSModuleTest.psm1 b/tests/outputTestRepo/outputs/modules/PSModuleTest/PSModuleTest.psm1 new file mode 100644 index 00000000..ffcb5734 --- /dev/null +++ b/tests/outputTestRepo/outputs/modules/PSModuleTest/PSModuleTest.psm1 @@ -0,0 +1,572 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] +[CmdletBinding()] +param() + +$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath) +$script:PSModuleInfo = Test-ModuleManifest -Path "$PSScriptRoot\$baseName.psd1" +$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ } +$scriptName = $script:PSModuleInfo.Name +Write-Debug "[$scriptName] - Importing module" +#region Data importer +Write-Debug "[$scriptName] - [data] - Processing folder" +$dataFolder = (Join-Path $PSScriptRoot 'data') +Write-Debug "[$scriptName] - [data] - [$dataFolder]" +Get-ChildItem -Path "$dataFolder" -Recurse -Force -Include '*.psd1' -ErrorAction SilentlyContinue | ForEach-Object { + Write-Debug "[$scriptName] - [data] - [$($_.BaseName)] - Importing" + New-Variable -Name $_.BaseName -Value (Import-PowerShellDataFile -Path $_.FullName) -Force + Write-Debug "[$scriptName] - [data] - [$($_.BaseName)] - Done" +} +Write-Debug "[$scriptName] - [data] - Done" +#endregion Data importer +#region [init] +Write-Debug "[$scriptName] - [init] - Processing folder" +#region [init] - [initializer] +Write-Debug "[$scriptName] - [init] - [initializer] - Importing" +Write-Verbose '-------------------------------' +Write-Verbose '--- THIS IS AN INITIALIZER ---' +Write-Verbose '-------------------------------' +Write-Debug "[$scriptName] - [init] - [initializer] - Done" +#endregion [init] - [initializer] +Write-Debug "[$scriptName] - [init] - Done" +#endregion [init] +#region [classes] - [private] +Write-Debug "[$scriptName] - [classes] - [private] - Processing folder" +#region [classes] - [private] - [SecretWriter] +Write-Debug "[$scriptName] - [classes] - [private] - [SecretWriter] - Importing" +class SecretWriter { + [string] $Alias + [string] $Name + [string] $Secret + + SecretWriter([string] $alias, [string] $name, [string] $secret) { + $this.Alias = $alias + $this.Name = $name + $this.Secret = $secret + } + + [string] GetAlias() { + return $this.Alias + } +} +Write-Debug "[$scriptName] - [classes] - [private] - [SecretWriter] - Done" +#endregion [classes] - [private] - [SecretWriter] +Write-Debug "[$scriptName] - [classes] - [private] - Done" +#endregion [classes] - [private] +#region [classes] - [public] +Write-Debug "[$scriptName] - [classes] - [public] - Processing folder" +#region [classes] - [public] - [Book] +Write-Debug "[$scriptName] - [classes] - [public] - [Book] - Importing" +class Book { + # Class properties + [string] $Title + [string] $Author + [string] $Synopsis + [string] $Publisher + [datetime] $PublishDate + [int] $PageCount + [string[]] $Tags + # Default constructor + Book() { $this.Init(@{}) } + # Convenience constructor from hashtable + Book([hashtable]$Properties) { $this.Init($Properties) } + # Common constructor for title and author + Book([string]$Title, [string]$Author) { + $this.Init(@{Title = $Title; Author = $Author }) + } + # Shared initializer method + [void] Init([hashtable]$Properties) { + foreach ($Property in $Properties.Keys) { + $this.$Property = $Properties.$Property + } + } + # Method to calculate reading time as 2 minutes per page + [timespan] GetReadingTime() { + if ($this.PageCount -le 0) { + throw 'Unable to determine reading time from page count.' + } + $Minutes = $this.PageCount * 2 + return [timespan]::new(0, $Minutes, 0) + } + # Method to calculate how long ago a book was published + [timespan] GetPublishedAge() { + if ( + $null -eq $this.PublishDate -or + $this.PublishDate -eq [datetime]::MinValue + ) { throw 'PublishDate not defined' } + + return (Get-Date) - $this.PublishDate + } + # Method to return a string representation of the book + [string] ToString() { + return "$($this.Title) by $($this.Author) ($($this.PublishDate.Year))" + } +} + +class BookList { + # Static property to hold the list of books + static [System.Collections.Generic.List[Book]] $Books + # Static method to initialize the list of books. Called in the other + # static methods to avoid needing to explicit initialize the value. + static [void] Initialize() { [BookList]::Initialize($false) } + static [bool] Initialize([bool]$force) { + if ([BookList]::Books.Count -gt 0 -and -not $force) { + return $false + } + + [BookList]::Books = [System.Collections.Generic.List[Book]]::new() + + return $true + } + # Ensure a book is valid for the list. + static [void] Validate([book]$Book) { + $Prefix = @( + 'Book validation failed: Book must be defined with the Title,' + 'Author, and PublishDate properties, but' + ) -join ' ' + if ($null -eq $Book) { throw "$Prefix was null" } + if ([string]::IsNullOrEmpty($Book.Title)) { + throw "$Prefix Title wasn't defined" + } + if ([string]::IsNullOrEmpty($Book.Author)) { + throw "$Prefix Author wasn't defined" + } + if ([datetime]::MinValue -eq $Book.PublishDate) { + throw "$Prefix PublishDate wasn't defined" + } + } + # Static methods to manage the list of books. + # Add a book if it's not already in the list. + static [void] Add([Book]$Book) { + [BookList]::Initialize() + [BookList]::Validate($Book) + if ([BookList]::Books.Contains($Book)) { + throw "Book '$Book' already in list" + } + + $FindPredicate = { + param([Book]$b) + + $b.Title -eq $Book.Title -and + $b.Author -eq $Book.Author -and + $b.PublishDate -eq $Book.PublishDate + }.GetNewClosure() + if ([BookList]::Books.Find($FindPredicate)) { + throw "Book '$Book' already in list" + } + + [BookList]::Books.Add($Book) + } + # Clear the list of books. + static [void] Clear() { + [BookList]::Initialize() + [BookList]::Books.Clear() + } + # Find a specific book using a filtering scriptblock. + static [Book] Find([scriptblock]$Predicate) { + [BookList]::Initialize() + return [BookList]::Books.Find($Predicate) + } + # Find every book matching the filtering scriptblock. + static [Book[]] FindAll([scriptblock]$Predicate) { + [BookList]::Initialize() + return [BookList]::Books.FindAll($Predicate) + } + # Remove a specific book. + static [void] Remove([Book]$Book) { + [BookList]::Initialize() + [BookList]::Books.Remove($Book) + } + # Remove a book by property value. + static [void] RemoveBy([string]$Property, [string]$Value) { + [BookList]::Initialize() + $Index = [BookList]::Books.FindIndex({ + param($b) + $b.$Property -eq $Value + }.GetNewClosure()) + if ($Index -ge 0) { + [BookList]::Books.RemoveAt($Index) + } + } +} + +enum Binding { + Hardcover + Paperback + EBook +} + +enum Genre { + Mystery + Thriller + Romance + ScienceFiction + Fantasy + Horror +} +Write-Debug "[$scriptName] - [classes] - [public] - [Book] - Done" +#endregion [classes] - [public] - [Book] +Write-Debug "[$scriptName] - [classes] - [public] - Done" +#endregion [classes] - [public] +#region [functions] - [private] +Write-Debug "[$scriptName] - [functions] - [private] - Processing folder" +#region [functions] - [private] - [Get-InternalPSModule] +Write-Debug "[$scriptName] - [functions] - [private] - [Get-InternalPSModule] - Importing" +function Get-InternalPSModule { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} +Write-Debug "[$scriptName] - [functions] - [private] - [Get-InternalPSModule] - Done" +#endregion [functions] - [private] - [Get-InternalPSModule] +#region [functions] - [private] - [Set-InternalPSModule] +Write-Debug "[$scriptName] - [functions] - [private] - [Set-InternalPSModule] - Importing" +function Set-InternalPSModule { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Reason for suppressing' + )] + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} +Write-Debug "[$scriptName] - [functions] - [private] - [Set-InternalPSModule] - Done" +#endregion [functions] - [private] - [Set-InternalPSModule] +Write-Debug "[$scriptName] - [functions] - [private] - Done" +#endregion [functions] - [private] +#region [functions] - [public] +Write-Debug "[$scriptName] - [functions] - [public] - Processing folder" +#region [functions] - [public] - [Test-PSModuleTest] +Write-Debug "[$scriptName] - [functions] - [public] - [Test-PSModuleTest] - Importing" +function Test-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} +Write-Debug "[$scriptName] - [functions] - [public] - [Test-PSModuleTest] - Done" +#endregion [functions] - [public] - [Test-PSModuleTest] +#region [functions] - [public] - [PSModule] +Write-Debug "[$scriptName] - [functions] - [public] - [PSModule] - Processing folder" +#region [functions] - [public] - [PSModule] - [Get-PSModuleTest] +Write-Debug "[$scriptName] - [functions] - [public] - [PSModule] - [Get-PSModuleTest] - Importing" +#Requires -Modules Utilities +#Requires -Modules @{ ModuleName = 'PSSemVer'; RequiredVersion = '1.0.0' } +#Requires -Modules @{ ModuleName = 'DynamicParams'; ModuleVersion = '1.1.8' } +#Requires -Modules @{ ModuleName = 'Store'; ModuleVersion = '0.3.1' } + +function Get-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} +Write-Debug "[$scriptName] - [functions] - [public] - [PSModule] - [Get-PSModuleTest] - Done" +#endregion [functions] - [public] - [PSModule] - [Get-PSModuleTest] +#region [functions] - [public] - [PSModule] - [New-PSModuleTest] +Write-Debug "[$scriptName] - [functions] - [public] - [PSModule] - [New-PSModuleTest] - Importing" +#Requires -Modules @{ModuleName='PSSemVer'; ModuleVersion='1.0'} + +function New-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + + .NOTES + Testing if a module can have a [Markdown based link](https://example.com). + !"#¤%&/()=?`´^¨*'-_+§½{[]}<>|@£$€¥¢:;.," + \[This is a test\] + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Reason for suppressing' + )] + [Alias('New-PSModuleTestAlias1')] + [Alias('New-PSModuleTestAlias2')] + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} + +New-Alias New-PSModuleTestAlias3 New-PSModuleTest +New-Alias -Name New-PSModuleTestAlias4 -Value New-PSModuleTest + + +Set-Alias New-PSModuleTestAlias5 New-PSModuleTest +Write-Debug "[$scriptName] - [functions] - [public] - [PSModule] - [New-PSModuleTest] - Done" +#endregion [functions] - [public] - [PSModule] - [New-PSModuleTest] +Write-Debug "[$scriptName] - [functions] - [public] - [PSModule] - Done" +#endregion [functions] - [public] - [PSModule] +#region [functions] - [public] - [SomethingElse] +Write-Debug "[$scriptName] - [functions] - [public] - [SomethingElse] - Processing folder" +#region [functions] - [public] - [SomethingElse] - [Set-PSModuleTest] +Write-Debug "[$scriptName] - [functions] - [public] - [SomethingElse] - [Set-PSModuleTest] - Importing" +function Set-PSModuleTest { + <# + .SYNOPSIS + Performs tests on a module. + + .EXAMPLE + Test-PSModule -Name 'World' + + "Hello, World!" + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', + Justification = 'Reason for suppressing' + )] + [CmdletBinding()] + param ( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} +Write-Debug "[$scriptName] - [functions] - [public] - [SomethingElse] - [Set-PSModuleTest] - Done" +#endregion [functions] - [public] - [SomethingElse] - [Set-PSModuleTest] +Write-Debug "[$scriptName] - [functions] - [public] - [SomethingElse] - Done" +#endregion [functions] - [public] - [SomethingElse] +Write-Debug "[$scriptName] - [functions] - [public] - Done" +#endregion [functions] - [public] +#region [variables] - [private] +Write-Debug "[$scriptName] - [variables] - [private] - Processing folder" +#region [variables] - [private] - [PrivateVariables] +Write-Debug "[$scriptName] - [variables] - [private] - [PrivateVariables] - Importing" +$script:HabitablePlanets = @( + @{ + Name = 'Earth' + Mass = 5.97 + Diameter = 12756 + DayLength = 24.0 + }, + @{ + Name = 'Mars' + Mass = 0.642 + Diameter = 6792 + DayLength = 24.7 + }, + @{ + Name = 'Proxima Centauri b' + Mass = 1.17 + Diameter = 11449 + DayLength = 5.15 + }, + @{ + Name = 'Kepler-442b' + Mass = 2.34 + Diameter = 11349 + DayLength = 5.7 + }, + @{ + Name = 'Kepler-452b' + Mass = 5.0 + Diameter = 17340 + DayLength = 20.0 + } +) + +$script:InhabitedPlanets = @( + @{ + Name = 'Earth' + Mass = 5.97 + Diameter = 12756 + DayLength = 24.0 + }, + @{ + Name = 'Mars' + Mass = 0.642 + Diameter = 6792 + DayLength = 24.7 + } +) +Write-Debug "[$scriptName] - [variables] - [private] - [PrivateVariables] - Done" +#endregion [variables] - [private] - [PrivateVariables] +Write-Debug "[$scriptName] - [variables] - [private] - Done" +#endregion [variables] - [private] +#region [variables] - [public] +Write-Debug "[$scriptName] - [variables] - [public] - Processing folder" +#region [variables] - [public] - [Moons] +Write-Debug "[$scriptName] - [variables] - [public] - [Moons] - Importing" +$script:Moons = @( + @{ + Planet = 'Earth' + Name = 'Moon' + } +) +Write-Debug "[$scriptName] - [variables] - [public] - [Moons] - Done" +#endregion [variables] - [public] - [Moons] +#region [variables] - [public] - [Planets] +Write-Debug "[$scriptName] - [variables] - [public] - [Planets] - Importing" +$script:Planets = @( + @{ + Name = 'Mercury' + Mass = 0.330 + Diameter = 4879 + DayLength = 4222.6 + }, + @{ + Name = 'Venus' + Mass = 4.87 + Diameter = 12104 + DayLength = 2802.0 + }, + @{ + Name = 'Earth' + Mass = 5.97 + Diameter = 12756 + DayLength = 24.0 + } +) +Write-Debug "[$scriptName] - [variables] - [public] - [Planets] - Done" +#endregion [variables] - [public] - [Planets] +#region [variables] - [public] - [SolarSystems] +Write-Debug "[$scriptName] - [variables] - [public] - [SolarSystems] - Importing" +$script:SolarSystems = @( + @{ + Name = 'Solar System' + Planets = $script:Planets + Moons = $script:Moons + }, + @{ + Name = 'Alpha Centauri' + Planets = @() + Moons = @() + }, + @{ + Name = 'Sirius' + Planets = @() + Moons = @() + } +) +Write-Debug "[$scriptName] - [variables] - [public] - [SolarSystems] - Done" +#endregion [variables] - [public] - [SolarSystems] +Write-Debug "[$scriptName] - [variables] - [public] - Done" +#endregion [variables] - [public] +#region [finally] +Write-Debug "[$scriptName] - [finally] - Importing" +Write-Verbose '------------------------------' +Write-Verbose '--- THIS IS A LAST LOADER ---' +Write-Verbose '------------------------------' +Write-Debug "[$scriptName] - [finally] - Done" +#endregion [finally] +#region Class exporter +# Get the internal TypeAccelerators class to use its static methods. +$TypeAcceleratorsClass = [psobject].Assembly.GetType( + 'System.Management.Automation.TypeAccelerators' +) +# Ensure none of the types would clobber an existing type accelerator. +# If a type accelerator with the same name exists, throw an exception. +$ExistingTypeAccelerators = $TypeAcceleratorsClass::Get +# Define the types to export with type accelerators. +$ExportableEnums = @( + [Binding] + [Genre] +) +$ExportableEnums | ForEach-Object { Write-Verbose "Exporting enum '$($_.FullName)'." } +foreach ($Type in $ExportableEnums) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + Write-Verbose "Enum already exists [$($Type.FullName)]. Skipping." + } else { + Write-Verbose "Importing enum '$Type'." + $TypeAcceleratorsClass::Add($Type.FullName, $Type) + } +} +$ExportableClasses = @( + [Book] + [BookList] +) +$ExportableClasses | ForEach-Object { Write-Verbose "Exporting class '$($_.FullName)'." } +foreach ($Type in $ExportableClasses) { + if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { + Write-Verbose "Class already exists [$($Type.FullName)]. Skipping." + } else { + Write-Verbose "Importing class '$Type'." + $TypeAcceleratorsClass::Add($Type.FullName, $Type) + } +} + +# Remove type accelerators when the module is removed. +$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { + foreach ($Type in ($ExportableEnums + $ExportableClasses)) { + $TypeAcceleratorsClass::Remove($Type.FullName) + } +}.GetNewClosure() +#endregion Class exporter +#region Member exporter +$exports = @{ + Alias = '*' + Cmdlet = '' + Function = @( + 'Get-PSModuleTest' + 'New-PSModuleTest' + 'Set-PSModuleTest' + 'Test-PSModuleTest' + ) + Variable = @( + 'Moons' + 'Planets' + 'SolarSystems' + ) +} +Export-ModuleMember @exports +#endregion Member exporter + diff --git a/tests/src/assemblies/LsonLib.dll b/tests/outputTestRepo/outputs/modules/PSModuleTest/assemblies/LsonLib.dll similarity index 100% rename from tests/src/assemblies/LsonLib.dll rename to tests/outputTestRepo/outputs/modules/PSModuleTest/assemblies/LsonLib.dll diff --git a/tests/src/data/Config.psd1 b/tests/outputTestRepo/outputs/modules/PSModuleTest/data/Config.psd1 similarity index 100% rename from tests/src/data/Config.psd1 rename to tests/outputTestRepo/outputs/modules/PSModuleTest/data/Config.psd1 diff --git a/tests/src/data/Settings.psd1 b/tests/outputTestRepo/outputs/modules/PSModuleTest/data/Settings.psd1 similarity index 100% rename from tests/src/data/Settings.psd1 rename to tests/outputTestRepo/outputs/modules/PSModuleTest/data/Settings.psd1 diff --git a/tests/src/formats/CultureInfo.Format.ps1xml b/tests/outputTestRepo/outputs/modules/PSModuleTest/formats/CultureInfo.Format.ps1xml similarity index 100% rename from tests/src/formats/CultureInfo.Format.ps1xml rename to tests/outputTestRepo/outputs/modules/PSModuleTest/formats/CultureInfo.Format.ps1xml diff --git a/tests/src/formats/Mygciview.Format.ps1xml b/tests/outputTestRepo/outputs/modules/PSModuleTest/formats/Mygciview.Format.ps1xml similarity index 100% rename from tests/src/formats/Mygciview.Format.ps1xml rename to tests/outputTestRepo/outputs/modules/PSModuleTest/formats/Mygciview.Format.ps1xml diff --git a/tests/src/modules/OtherPSModule.psm1 b/tests/outputTestRepo/outputs/modules/PSModuleTest/modules/OtherPSModule.psm1 similarity index 100% rename from tests/src/modules/OtherPSModule.psm1 rename to tests/outputTestRepo/outputs/modules/PSModuleTest/modules/OtherPSModule.psm1 diff --git a/tests/src/scripts/loader.ps1 b/tests/outputTestRepo/outputs/modules/PSModuleTest/scripts/loader.ps1 similarity index 100% rename from tests/src/scripts/loader.ps1 rename to tests/outputTestRepo/outputs/modules/PSModuleTest/scripts/loader.ps1 diff --git a/tests/src/types/DirectoryInfo.Types.ps1xml b/tests/outputTestRepo/outputs/modules/PSModuleTest/types/DirectoryInfo.Types.ps1xml similarity index 100% rename from tests/src/types/DirectoryInfo.Types.ps1xml rename to tests/outputTestRepo/outputs/modules/PSModuleTest/types/DirectoryInfo.Types.ps1xml diff --git a/tests/src/types/FileInfo.Types.ps1xml b/tests/outputTestRepo/outputs/modules/PSModuleTest/types/FileInfo.Types.ps1xml similarity index 100% rename from tests/src/types/FileInfo.Types.ps1xml rename to tests/outputTestRepo/outputs/modules/PSModuleTest/types/FileInfo.Types.ps1xml diff --git a/tests/tests/PSModuleTest.Tests.ps1 b/tests/outputTestRepo/tests/PSModuleTest.Tests.ps1 similarity index 100% rename from tests/tests/PSModuleTest.Tests.ps1 rename to tests/outputTestRepo/tests/PSModuleTest.Tests.ps1 diff --git a/tests/srcTestRepo/README.md b/tests/srcTestRepo/README.md new file mode 100644 index 00000000..b459e352 --- /dev/null +++ b/tests/srcTestRepo/README.md @@ -0,0 +1,3 @@ +# Test module + +This is a test readme. diff --git a/tests/icon/icon.png b/tests/srcTestRepo/icon/icon.png similarity index 100% rename from tests/icon/icon.png rename to tests/srcTestRepo/icon/icon.png diff --git a/tests/srcTestRepo/mkdocs.yml b/tests/srcTestRepo/mkdocs.yml new file mode 100644 index 00000000..df5e17ad --- /dev/null +++ b/tests/srcTestRepo/mkdocs.yml @@ -0,0 +1,75 @@ +site_name: -{{ REPO_NAME }}- +theme: + name: material + language: en + font: + text: Roboto + code: Sono + logo: Assets/icon.png + favicon: Assets/icon.png + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/link + name: Switch to dark mode + # Palette toggle for dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + toggle: + primary: black + accent: green + icon: material/toggle-switch-off-outline + name: Switch to light mode + # Palette toggle for light mode + - media: '(prefers-color-scheme: light)' + scheme: default + toggle: + primary: indigo + accent: green + icon: material/toggle-switch + name: Switch to system preference + icon: + repo: material/github + features: + - navigation.instant + - navigation.instant.progress + - navigation.indexes + - navigation.top + - navigation.tracking + - navigation.expand + - search.suggest + - search.highlight + +repo_name: -{{ REPO_OWNER }}-/-{{ REPO_NAME }}- +repo_url: https://github.com/-{{ REPO_OWNER }}-/-{{ REPO_NAME }}- + +plugins: + - search + +markdown_extensions: + - toc: + permalink: true # Adds a link icon to headings + - attr_list + - admonition + - md_in_html + - pymdownx.details # Enables collapsible admonitions + +extra: + social: + - icon: fontawesome/brands/discord + link: https://discord.gg/jedJWCPAhD + name: -{{ REPO_OWNER }}- on Discord + - icon: fontawesome/brands/github + link: https://github.com/-{{ REPO_OWNER }}-/ + name: -{{ REPO_OWNER }}- on GitHub + consent: + title: Cookie consent + description: >- + We use cookies to recognize your repeated visits and preferences, as well + as to measure the effectiveness of our documentation and whether users + find what they're searching for. With your consent, you're helping us to + make our documentation better. + actions: + - accept + - reject diff --git a/tests/srcWithManifest/assemblies/LsonLib.dll b/tests/srcTestRepo/src/assemblies/LsonLib.dll similarity index 100% rename from tests/srcWithManifest/assemblies/LsonLib.dll rename to tests/srcTestRepo/src/assemblies/LsonLib.dll diff --git a/tests/src/classes/private/SecretWriter.ps1 b/tests/srcTestRepo/src/classes/private/SecretWriter.ps1 similarity index 100% rename from tests/src/classes/private/SecretWriter.ps1 rename to tests/srcTestRepo/src/classes/private/SecretWriter.ps1 diff --git a/tests/src/classes/public/Book.ps1 b/tests/srcTestRepo/src/classes/public/Book.ps1 similarity index 100% rename from tests/src/classes/public/Book.ps1 rename to tests/srcTestRepo/src/classes/public/Book.ps1 diff --git a/tests/srcWithManifest/data/Config.psd1 b/tests/srcTestRepo/src/data/Config.psd1 similarity index 100% rename from tests/srcWithManifest/data/Config.psd1 rename to tests/srcTestRepo/src/data/Config.psd1 diff --git a/tests/srcWithManifest/data/Settings.psd1 b/tests/srcTestRepo/src/data/Settings.psd1 similarity index 100% rename from tests/srcWithManifest/data/Settings.psd1 rename to tests/srcTestRepo/src/data/Settings.psd1 diff --git a/tests/src/finally.ps1 b/tests/srcTestRepo/src/finally.ps1 similarity index 100% rename from tests/src/finally.ps1 rename to tests/srcTestRepo/src/finally.ps1 diff --git a/tests/srcWithManifest/formats/CultureInfo.Format.ps1xml b/tests/srcTestRepo/src/formats/CultureInfo.Format.ps1xml similarity index 100% rename from tests/srcWithManifest/formats/CultureInfo.Format.ps1xml rename to tests/srcTestRepo/src/formats/CultureInfo.Format.ps1xml diff --git a/tests/srcWithManifest/formats/Mygciview.Format.ps1xml b/tests/srcTestRepo/src/formats/Mygciview.Format.ps1xml similarity index 100% rename from tests/srcWithManifest/formats/Mygciview.Format.ps1xml rename to tests/srcTestRepo/src/formats/Mygciview.Format.ps1xml diff --git a/tests/src/functions/private/Get-InternalPSModule.ps1 b/tests/srcTestRepo/src/functions/private/Get-InternalPSModule.ps1 similarity index 100% rename from tests/src/functions/private/Get-InternalPSModule.ps1 rename to tests/srcTestRepo/src/functions/private/Get-InternalPSModule.ps1 diff --git a/tests/src/functions/private/Set-InternalPSModule.ps1 b/tests/srcTestRepo/src/functions/private/Set-InternalPSModule.ps1 similarity index 100% rename from tests/src/functions/private/Set-InternalPSModule.ps1 rename to tests/srcTestRepo/src/functions/private/Set-InternalPSModule.ps1 diff --git a/tests/src/functions/public/PSModule/Get-PSModuleTest.ps1 b/tests/srcTestRepo/src/functions/public/PSModule/Get-PSModuleTest.ps1 similarity index 100% rename from tests/src/functions/public/PSModule/Get-PSModuleTest.ps1 rename to tests/srcTestRepo/src/functions/public/PSModule/Get-PSModuleTest.ps1 diff --git a/tests/src/functions/public/PSModule/New-PSModuleTest.ps1 b/tests/srcTestRepo/src/functions/public/PSModule/New-PSModuleTest.ps1 similarity index 100% rename from tests/src/functions/public/PSModule/New-PSModuleTest.ps1 rename to tests/srcTestRepo/src/functions/public/PSModule/New-PSModuleTest.ps1 diff --git a/tests/srcWithManifest/functions/public/PSModule/PSModule.md b/tests/srcTestRepo/src/functions/public/PSModule/PSModule.md similarity index 100% rename from tests/srcWithManifest/functions/public/PSModule/PSModule.md rename to tests/srcTestRepo/src/functions/public/PSModule/PSModule.md diff --git a/tests/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 b/tests/srcTestRepo/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 similarity index 100% rename from tests/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 rename to tests/srcTestRepo/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 diff --git a/tests/srcWithManifest/functions/public/SomethingElse/SomethingElse.md b/tests/srcTestRepo/src/functions/public/SomethingElse/SomethingElse.md similarity index 100% rename from tests/srcWithManifest/functions/public/SomethingElse/SomethingElse.md rename to tests/srcTestRepo/src/functions/public/SomethingElse/SomethingElse.md diff --git a/tests/src/functions/public/Test-PSModuleTest.ps1 b/tests/srcTestRepo/src/functions/public/Test-PSModuleTest.ps1 similarity index 100% rename from tests/src/functions/public/Test-PSModuleTest.ps1 rename to tests/srcTestRepo/src/functions/public/Test-PSModuleTest.ps1 diff --git a/tests/src/header.ps1 b/tests/srcTestRepo/src/header.ps1 similarity index 100% rename from tests/src/header.ps1 rename to tests/srcTestRepo/src/header.ps1 diff --git a/tests/src/init/initializer.ps1 b/tests/srcTestRepo/src/init/initializer.ps1 similarity index 100% rename from tests/src/init/initializer.ps1 rename to tests/srcTestRepo/src/init/initializer.ps1 diff --git a/tests/srcWithManifest/modules/OtherPSModule.psm1 b/tests/srcTestRepo/src/modules/OtherPSModule.psm1 similarity index 100% rename from tests/srcWithManifest/modules/OtherPSModule.psm1 rename to tests/srcTestRepo/src/modules/OtherPSModule.psm1 diff --git a/tests/srcWithManifest/scripts/loader.ps1 b/tests/srcTestRepo/src/scripts/loader.ps1 similarity index 100% rename from tests/srcWithManifest/scripts/loader.ps1 rename to tests/srcTestRepo/src/scripts/loader.ps1 diff --git a/tests/srcWithManifest/types/DirectoryInfo.Types.ps1xml b/tests/srcTestRepo/src/types/DirectoryInfo.Types.ps1xml similarity index 100% rename from tests/srcWithManifest/types/DirectoryInfo.Types.ps1xml rename to tests/srcTestRepo/src/types/DirectoryInfo.Types.ps1xml diff --git a/tests/srcWithManifest/types/FileInfo.Types.ps1xml b/tests/srcTestRepo/src/types/FileInfo.Types.ps1xml similarity index 100% rename from tests/srcWithManifest/types/FileInfo.Types.ps1xml rename to tests/srcTestRepo/src/types/FileInfo.Types.ps1xml diff --git a/tests/src/variables/private/PrivateVariables.ps1 b/tests/srcTestRepo/src/variables/private/PrivateVariables.ps1 similarity index 100% rename from tests/src/variables/private/PrivateVariables.ps1 rename to tests/srcTestRepo/src/variables/private/PrivateVariables.ps1 diff --git a/tests/src/variables/public/Moons.ps1 b/tests/srcTestRepo/src/variables/public/Moons.ps1 similarity index 100% rename from tests/src/variables/public/Moons.ps1 rename to tests/srcTestRepo/src/variables/public/Moons.ps1 diff --git a/tests/src/variables/public/Planets.ps1 b/tests/srcTestRepo/src/variables/public/Planets.ps1 similarity index 100% rename from tests/src/variables/public/Planets.ps1 rename to tests/srcTestRepo/src/variables/public/Planets.ps1 diff --git a/tests/src/variables/public/SolarSystems.ps1 b/tests/srcTestRepo/src/variables/public/SolarSystems.ps1 similarity index 100% rename from tests/src/variables/public/SolarSystems.ps1 rename to tests/srcTestRepo/src/variables/public/SolarSystems.ps1 diff --git a/tests/srcTestRepo/tests/PSModuleTest.Tests.ps1 b/tests/srcTestRepo/tests/PSModuleTest.Tests.ps1 new file mode 100644 index 00000000..2be65edb --- /dev/null +++ b/tests/srcTestRepo/tests/PSModuleTest.Tests.ps1 @@ -0,0 +1,60 @@ +[CmdletBinding()] +Param( + # Path to the module to test. + [Parameter()] + [string] $Path +) + +Write-Verbose "Path to the module: [$Path]" -Verbose +Describe 'Environment Variables are available' { + It 'Should be available [<_>]' -ForEach @( + 'TEST_APP_ENT_CLIENT_ID', + 'TEST_APP_ENT_PRIVATE_KEY', + 'TEST_APP_ORG_CLIENT_ID', + 'TEST_APP_ORG_PRIVATE_KEY', + 'TEST_USER_ORG_FG_PAT', + 'TEST_USER_USER_FG_PAT', + 'TEST_USER_PAT' + ) { + $name = $_ + Write-Verbose "Environment variable: [$name]" -Verbose + Get-ChildItem env: | Where-Object { $_.Name -eq $name } | Should -Not -BeNullOrEmpty + } +} + +Describe 'PSModuleTest.Tests.ps1' { + Context 'Function: Test-PSModuleTest' { + It 'Should be able to call the function' { + Write-Verbose (Test-PSModuleTest -Name 'World' | Out-String) -Verbose + Test-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' + } + } + + Context 'Function: Get-PSModuleTest' { + It 'Should be able to call the function' { + Write-Verbose (Get-PSModuleTest -Name 'World' | Out-String) -Verbose + Get-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' + } + } + + Context 'Function: New-PSModuleTest' { + It 'Should be able to call the function' { + Write-Verbose (New-PSModuleTest -Name 'World' | Out-String) -Verbose + New-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' + } + } + + Context 'Function: Set-PSModuleTest' { + It 'Should be able to call the function' { + Write-Verbose (Set-PSModuleTest -Name 'World' | Out-String) -Verbose + Set-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' + } + } + + Context 'Variables' { + It "Exports a variable for SolarSystems that contains 'Solar System'" { + Write-Verbose ($SolarSystems | Out-String) -Verbose + $SolarSystems[0].Name | Should -Be 'Solar System' + } + } +} diff --git a/tests/srcWithManifest/manifest.psd1 b/tests/srcWithManifest/manifest.psd1 deleted file mode 100644 index 73e59522..00000000 --- a/tests/srcWithManifest/manifest.psd1 +++ /dev/null @@ -1,4 +0,0 @@ -@{ - Author = 'Author' - PowerShellVersion = '5.1' -} diff --git a/tests/srcWithManifestTestRepo/README.md b/tests/srcWithManifestTestRepo/README.md new file mode 100644 index 00000000..b459e352 --- /dev/null +++ b/tests/srcWithManifestTestRepo/README.md @@ -0,0 +1,3 @@ +# Test module + +This is a test readme. diff --git a/tests/srcWithManifestTestRepo/icon/icon.png b/tests/srcWithManifestTestRepo/icon/icon.png new file mode 100644 index 00000000..be83fd5f Binary files /dev/null and b/tests/srcWithManifestTestRepo/icon/icon.png differ diff --git a/tests/srcWithManifestTestRepo/mkdocs.yml b/tests/srcWithManifestTestRepo/mkdocs.yml new file mode 100644 index 00000000..df5e17ad --- /dev/null +++ b/tests/srcWithManifestTestRepo/mkdocs.yml @@ -0,0 +1,75 @@ +site_name: -{{ REPO_NAME }}- +theme: + name: material + language: en + font: + text: Roboto + code: Sono + logo: Assets/icon.png + favicon: Assets/icon.png + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/link + name: Switch to dark mode + # Palette toggle for dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + toggle: + primary: black + accent: green + icon: material/toggle-switch-off-outline + name: Switch to light mode + # Palette toggle for light mode + - media: '(prefers-color-scheme: light)' + scheme: default + toggle: + primary: indigo + accent: green + icon: material/toggle-switch + name: Switch to system preference + icon: + repo: material/github + features: + - navigation.instant + - navigation.instant.progress + - navigation.indexes + - navigation.top + - navigation.tracking + - navigation.expand + - search.suggest + - search.highlight + +repo_name: -{{ REPO_OWNER }}-/-{{ REPO_NAME }}- +repo_url: https://github.com/-{{ REPO_OWNER }}-/-{{ REPO_NAME }}- + +plugins: + - search + +markdown_extensions: + - toc: + permalink: true # Adds a link icon to headings + - attr_list + - admonition + - md_in_html + - pymdownx.details # Enables collapsible admonitions + +extra: + social: + - icon: fontawesome/brands/discord + link: https://discord.gg/jedJWCPAhD + name: -{{ REPO_OWNER }}- on Discord + - icon: fontawesome/brands/github + link: https://github.com/-{{ REPO_OWNER }}-/ + name: -{{ REPO_OWNER }}- on GitHub + consent: + title: Cookie consent + description: >- + We use cookies to recognize your repeated visits and preferences, as well + as to measure the effectiveness of our documentation and whether users + find what they're searching for. With your consent, you're helping us to + make our documentation better. + actions: + - accept + - reject diff --git a/tests/srcWithManifestTestRepo/src/assemblies/LsonLib.dll b/tests/srcWithManifestTestRepo/src/assemblies/LsonLib.dll new file mode 100644 index 00000000..36618070 Binary files /dev/null and b/tests/srcWithManifestTestRepo/src/assemblies/LsonLib.dll differ diff --git a/tests/srcWithManifest/classes/private/SecretWriter.ps1 b/tests/srcWithManifestTestRepo/src/classes/private/SecretWriter.ps1 similarity index 100% rename from tests/srcWithManifest/classes/private/SecretWriter.ps1 rename to tests/srcWithManifestTestRepo/src/classes/private/SecretWriter.ps1 diff --git a/tests/srcWithManifest/classes/public/Book.ps1 b/tests/srcWithManifestTestRepo/src/classes/public/Book.ps1 similarity index 100% rename from tests/srcWithManifest/classes/public/Book.ps1 rename to tests/srcWithManifestTestRepo/src/classes/public/Book.ps1 diff --git a/tests/srcWithManifestTestRepo/src/data/Config.psd1 b/tests/srcWithManifestTestRepo/src/data/Config.psd1 new file mode 100644 index 00000000..fea44669 --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/data/Config.psd1 @@ -0,0 +1,3 @@ +@{ + RandomKey = 'RandomValue' +} diff --git a/tests/srcWithManifestTestRepo/src/data/Settings.psd1 b/tests/srcWithManifestTestRepo/src/data/Settings.psd1 new file mode 100644 index 00000000..bcfa7b47 --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/data/Settings.psd1 @@ -0,0 +1,3 @@ +@{ + RandomSetting = 'RandomSettingValue' +} diff --git a/tests/srcWithManifest/finally.ps1 b/tests/srcWithManifestTestRepo/src/finally.ps1 similarity index 100% rename from tests/srcWithManifest/finally.ps1 rename to tests/srcWithManifestTestRepo/src/finally.ps1 diff --git a/tests/srcWithManifestTestRepo/src/formats/CultureInfo.Format.ps1xml b/tests/srcWithManifestTestRepo/src/formats/CultureInfo.Format.ps1xml new file mode 100644 index 00000000..a715e08a --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/formats/CultureInfo.Format.ps1xml @@ -0,0 +1,37 @@ + + + + + System.Globalization.CultureInfo + + System.Globalization.CultureInfo + + + + + 16 + + + 16 + + + + + + + + LCID + + + Name + + + DisplayName + + + + + + + + diff --git a/tests/srcWithManifestTestRepo/src/formats/Mygciview.Format.ps1xml b/tests/srcWithManifestTestRepo/src/formats/Mygciview.Format.ps1xml new file mode 100644 index 00000000..4c972c2c --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/formats/Mygciview.Format.ps1xml @@ -0,0 +1,65 @@ + + + + + mygciview + + System.IO.DirectoryInfo + System.IO.FileInfo + + + PSParentPath + + + + + + 7 + Left + + + + 26 + Right + + + + 26 + Right + + + + 14 + Right + + + + Left + + + + + + + + ModeWithoutHardLink + + + LastWriteTime + + + CreationTime + + + Length + + + Name + + + + + + + + diff --git a/tests/srcWithManifest/functions/private/Get-InternalPSModule.ps1 b/tests/srcWithManifestTestRepo/src/functions/private/Get-InternalPSModule.ps1 similarity index 100% rename from tests/srcWithManifest/functions/private/Get-InternalPSModule.ps1 rename to tests/srcWithManifestTestRepo/src/functions/private/Get-InternalPSModule.ps1 diff --git a/tests/srcWithManifest/functions/private/Set-InternalPSModule.ps1 b/tests/srcWithManifestTestRepo/src/functions/private/Set-InternalPSModule.ps1 similarity index 100% rename from tests/srcWithManifest/functions/private/Set-InternalPSModule.ps1 rename to tests/srcWithManifestTestRepo/src/functions/private/Set-InternalPSModule.ps1 diff --git a/tests/srcWithManifest/functions/public/PSModule/Get-PSModuleTest.ps1 b/tests/srcWithManifestTestRepo/src/functions/public/PSModule/Get-PSModuleTest.ps1 similarity index 100% rename from tests/srcWithManifest/functions/public/PSModule/Get-PSModuleTest.ps1 rename to tests/srcWithManifestTestRepo/src/functions/public/PSModule/Get-PSModuleTest.ps1 diff --git a/tests/srcWithManifest/functions/public/PSModule/New-PSModuleTest.ps1 b/tests/srcWithManifestTestRepo/src/functions/public/PSModule/New-PSModuleTest.ps1 similarity index 100% rename from tests/srcWithManifest/functions/public/PSModule/New-PSModuleTest.ps1 rename to tests/srcWithManifestTestRepo/src/functions/public/PSModule/New-PSModuleTest.ps1 diff --git a/tests/srcWithManifestTestRepo/src/functions/public/PSModule/PSModule.md b/tests/srcWithManifestTestRepo/src/functions/public/PSModule/PSModule.md new file mode 100644 index 00000000..79741cf4 --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/functions/public/PSModule/PSModule.md @@ -0,0 +1 @@ +# This is PSModule diff --git a/tests/srcWithManifest/functions/public/SomethingElse/Set-PSModuleTest.ps1 b/tests/srcWithManifestTestRepo/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 similarity index 100% rename from tests/srcWithManifest/functions/public/SomethingElse/Set-PSModuleTest.ps1 rename to tests/srcWithManifestTestRepo/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 diff --git a/tests/srcWithManifestTestRepo/src/functions/public/SomethingElse/SomethingElse.md b/tests/srcWithManifestTestRepo/src/functions/public/SomethingElse/SomethingElse.md new file mode 100644 index 00000000..d9f7e9ee --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/functions/public/SomethingElse/SomethingElse.md @@ -0,0 +1 @@ +# This is SomethingElse diff --git a/tests/srcWithManifest/functions/public/Test-PSModuleTest.ps1 b/tests/srcWithManifestTestRepo/src/functions/public/Test-PSModuleTest.ps1 similarity index 67% rename from tests/srcWithManifest/functions/public/Test-PSModuleTest.ps1 rename to tests/srcWithManifestTestRepo/src/functions/public/Test-PSModuleTest.ps1 index 26be2b9b..4056e2f6 100644 --- a/tests/srcWithManifest/functions/public/Test-PSModuleTest.ps1 +++ b/tests/srcWithManifestTestRepo/src/functions/public/Test-PSModuleTest.ps1 @@ -1,4 +1,5 @@ -function Test-PSModuleTest { +#SkipTest:Verbose:Just want to test that a function can have multiple skips. +function Test-PSModuleTest { <# .SYNOPSIS Performs tests on a module. @@ -15,4 +16,5 @@ [string] $Name ) Write-Output "Hello, $Name!" + Write-Verbose 'Verbose message' -Verbose } diff --git a/tests/srcWithManifestTestRepo/src/functions/public/completers.ps1 b/tests/srcWithManifestTestRepo/src/functions/public/completers.ps1 new file mode 100644 index 00000000..6b1adbb7 --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/functions/public/completers.ps1 @@ -0,0 +1,8 @@ +Register-ArgumentCompleter -CommandName New-PSModuleTest -ParameterName Name -ScriptBlock { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters + + 'Alice', 'Bob', 'Charlie' | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } +} diff --git a/tests/srcWithManifest/header.ps1 b/tests/srcWithManifestTestRepo/src/header.ps1 similarity index 100% rename from tests/srcWithManifest/header.ps1 rename to tests/srcWithManifestTestRepo/src/header.ps1 diff --git a/tests/srcWithManifest/init/initializer.ps1 b/tests/srcWithManifestTestRepo/src/init/initializer.ps1 similarity index 100% rename from tests/srcWithManifest/init/initializer.ps1 rename to tests/srcWithManifestTestRepo/src/init/initializer.ps1 diff --git a/tests/srcWithManifestTestRepo/src/manifest.psd1 b/tests/srcWithManifestTestRepo/src/manifest.psd1 new file mode 100644 index 00000000..77348f13 --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/manifest.psd1 @@ -0,0 +1,3 @@ +@{ + Author = 'Author' +} diff --git a/tests/srcWithManifestTestRepo/src/modules/OtherPSModule.psm1 b/tests/srcWithManifestTestRepo/src/modules/OtherPSModule.psm1 new file mode 100644 index 00000000..5d6af8ea --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/modules/OtherPSModule.psm1 @@ -0,0 +1,19 @@ +function Get-OtherPSModule { + <# + .SYNOPSIS + Performs tests on a module. + + .DESCRIPTION + A longer description of the function. + + .EXAMPLE + Get-OtherPSModule -Name 'World' + #> + [CmdletBinding()] + param( + # Name of the person to greet. + [Parameter(Mandatory)] + [string] $Name + ) + Write-Output "Hello, $Name!" +} diff --git a/tests/srcWithManifestTestRepo/src/scripts/loader.ps1 b/tests/srcWithManifestTestRepo/src/scripts/loader.ps1 new file mode 100644 index 00000000..973735ad --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/scripts/loader.ps1 @@ -0,0 +1,3 @@ +Write-Verbose '-------------------------' +Write-Verbose '--- THIS IS A LOADER ---' +Write-Verbose '-------------------------' diff --git a/tests/srcWithManifestTestRepo/src/types/DirectoryInfo.Types.ps1xml b/tests/srcWithManifestTestRepo/src/types/DirectoryInfo.Types.ps1xml new file mode 100644 index 00000000..aef538b2 --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/types/DirectoryInfo.Types.ps1xml @@ -0,0 +1,21 @@ + + + + System.IO.FileInfo + + + Status + Success + + + + + System.IO.DirectoryInfo + + + Status + Success + + + + diff --git a/tests/srcWithManifestTestRepo/src/types/FileInfo.Types.ps1xml b/tests/srcWithManifestTestRepo/src/types/FileInfo.Types.ps1xml new file mode 100644 index 00000000..4cfaf6b8 --- /dev/null +++ b/tests/srcWithManifestTestRepo/src/types/FileInfo.Types.ps1xml @@ -0,0 +1,14 @@ + + + + System.IO.FileInfo + + + Age + + ((Get-Date) - ($this.CreationTime)).Days + + + + + diff --git a/tests/srcWithManifest/variables/private/PrivateVariables.ps1 b/tests/srcWithManifestTestRepo/src/variables/private/PrivateVariables.ps1 similarity index 100% rename from tests/srcWithManifest/variables/private/PrivateVariables.ps1 rename to tests/srcWithManifestTestRepo/src/variables/private/PrivateVariables.ps1 diff --git a/tests/srcWithManifest/variables/public/Moons.ps1 b/tests/srcWithManifestTestRepo/src/variables/public/Moons.ps1 similarity index 100% rename from tests/srcWithManifest/variables/public/Moons.ps1 rename to tests/srcWithManifestTestRepo/src/variables/public/Moons.ps1 diff --git a/tests/srcWithManifest/variables/public/Planets.ps1 b/tests/srcWithManifestTestRepo/src/variables/public/Planets.ps1 similarity index 100% rename from tests/srcWithManifest/variables/public/Planets.ps1 rename to tests/srcWithManifestTestRepo/src/variables/public/Planets.ps1 diff --git a/tests/srcWithManifest/variables/public/SolarSystems.ps1 b/tests/srcWithManifestTestRepo/src/variables/public/SolarSystems.ps1 similarity index 100% rename from tests/srcWithManifest/variables/public/SolarSystems.ps1 rename to tests/srcWithManifestTestRepo/src/variables/public/SolarSystems.ps1 diff --git a/tests/srcWithManifestTestRepo/tests/PSModuleTest.Tests.ps1 b/tests/srcWithManifestTestRepo/tests/PSModuleTest.Tests.ps1 new file mode 100644 index 00000000..2be65edb --- /dev/null +++ b/tests/srcWithManifestTestRepo/tests/PSModuleTest.Tests.ps1 @@ -0,0 +1,60 @@ +[CmdletBinding()] +Param( + # Path to the module to test. + [Parameter()] + [string] $Path +) + +Write-Verbose "Path to the module: [$Path]" -Verbose +Describe 'Environment Variables are available' { + It 'Should be available [<_>]' -ForEach @( + 'TEST_APP_ENT_CLIENT_ID', + 'TEST_APP_ENT_PRIVATE_KEY', + 'TEST_APP_ORG_CLIENT_ID', + 'TEST_APP_ORG_PRIVATE_KEY', + 'TEST_USER_ORG_FG_PAT', + 'TEST_USER_USER_FG_PAT', + 'TEST_USER_PAT' + ) { + $name = $_ + Write-Verbose "Environment variable: [$name]" -Verbose + Get-ChildItem env: | Where-Object { $_.Name -eq $name } | Should -Not -BeNullOrEmpty + } +} + +Describe 'PSModuleTest.Tests.ps1' { + Context 'Function: Test-PSModuleTest' { + It 'Should be able to call the function' { + Write-Verbose (Test-PSModuleTest -Name 'World' | Out-String) -Verbose + Test-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' + } + } + + Context 'Function: Get-PSModuleTest' { + It 'Should be able to call the function' { + Write-Verbose (Get-PSModuleTest -Name 'World' | Out-String) -Verbose + Get-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' + } + } + + Context 'Function: New-PSModuleTest' { + It 'Should be able to call the function' { + Write-Verbose (New-PSModuleTest -Name 'World' | Out-String) -Verbose + New-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' + } + } + + Context 'Function: Set-PSModuleTest' { + It 'Should be able to call the function' { + Write-Verbose (Set-PSModuleTest -Name 'World' | Out-String) -Verbose + Set-PSModuleTest -Name 'World' | Should -Be 'Hello, World!' + } + } + + Context 'Variables' { + It "Exports a variable for SolarSystems that contains 'Solar System'" { + Write-Verbose ($SolarSystems | Out-String) -Verbose + $SolarSystems[0].Name | Should -Be 'Solar System' + } + } +} diff --git a/tests/srcWithManifestTestRepo/tools/1-build.ps1 b/tests/srcWithManifestTestRepo/tools/1-build.ps1 new file mode 100644 index 00000000..f3a17f23 --- /dev/null +++ b/tests/srcWithManifestTestRepo/tools/1-build.ps1 @@ -0,0 +1 @@ +Write-Host "1 - Build script executed." diff --git a/tests/srcWithManifestTestRepo/tools/2-build.ps1 b/tests/srcWithManifestTestRepo/tools/2-build.ps1 new file mode 100644 index 00000000..d839b516 --- /dev/null +++ b/tests/srcWithManifestTestRepo/tools/2-build.ps1 @@ -0,0 +1 @@ +Write-Host "2 - Build script executed."