From 3a1fc4ca952a62a954224c11056f19e957b1f60d Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 13 Jul 2025 19:07:48 -0700 Subject: [PATCH 1/4] Refactor build process and update documentation - Added a new cspell.json file for spell checking with custom words related to the project. - Updated documentation for Get-PlasterTemplate, Invoke-Plaster, New-PlasterManifest, and Test-PlasterManifest cmdlets to include the new -ProgressAction parameter. - Modified psakeFile.ps1 to set up build preferences and dependencies more efficiently. - Introduced requirements.psd1 to manage module dependencies and their versions. --- .vscode/settings.json | 4 +- .vscode/tasks.json | 49 +- Plaster/Plaster.psd1 | 31 +- Plaster/Plaster.psm1 | 93 +-- .../GetPlasterManifestPathForCulture.ps1 | 38 + .../Private/InitializePredefinedVariables.ps1 | 31 + Plaster/Private/Invoke-PlasterOperation.ps1 | 26 + Plaster/Private/Write-PlasterLog.ps1 | 24 + .../Get-ModuleExtension.ps1} | 20 +- .../Get-PlasterTemplate.ps1} | 18 +- .../Invoke-Plaster.ps1} | 393 ++++------ .../New-PlasterManifest.ps1} | 0 .../Test-PlasterManifest.ps1} | 0 build.ps1 | 548 ++------------ build.psake.ps1 | 693 ------------------ cspell.json | 21 + docs/en-US/Get-ModuleExtension.md | 106 +++ docs/en-US/Get-PlasterTemplate.md | 20 +- docs/en-US/Invoke-Plaster.md | 19 +- docs/en-US/New-PlasterManifest.md | 18 +- docs/en-US/Test-PlasterManifest.md | 17 +- psakeFile.ps1 | 145 +--- requirements.psd1 | 26 + 23 files changed, 618 insertions(+), 1722 deletions(-) create mode 100644 Plaster/Private/GetPlasterManifestPathForCulture.ps1 create mode 100644 Plaster/Private/InitializePredefinedVariables.ps1 create mode 100644 Plaster/Private/Invoke-PlasterOperation.ps1 create mode 100644 Plaster/Private/Write-PlasterLog.ps1 rename Plaster/{GetModuleExtension.ps1 => Public/Get-ModuleExtension.ps1} (84%) rename Plaster/{GetPlasterTemplate.ps1 => Public/Get-PlasterTemplate.ps1} (88%) rename Plaster/{InvokePlaster.ps1 => Public/Invoke-Plaster.ps1} (84%) rename Plaster/{NewPlasterManifest.ps1 => Public/New-PlasterManifest.ps1} (100%) rename Plaster/{TestPlasterManifest.ps1 => Public/Test-PlasterManifest.ps1} (100%) delete mode 100644 build.psake.ps1 create mode 100644 cspell.json create mode 100644 docs/en-US/Get-ModuleExtension.md create mode 100644 requirements.psd1 diff --git a/.vscode/settings.json b/.vscode/settings.json index b76ef60..a139506 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,8 +3,10 @@ // When enabled, will trim trailing whitespace when you save a file. "files.trimTrailingWhitespace": true, "search.exclude": { - "Release": true + "Release": true, + "Output": true, }, + "editor.tabSize": 4, //-------- PowerShell Configuration -------- // Use a custom PowerShell Script Analyzer settings file for this workspace. // Relative paths for this setting are always relative to the workspace root dir. diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4449d94..886c81c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,13 +2,17 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", - // Start PowerShell "windows": { "options": { "shell": { - "executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", - "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command" ] + "executable": "pwsh.exe", + "args": [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command" + ] } } }, @@ -16,7 +20,10 @@ "options": { "shell": { "executable": "/usr/bin/pwsh", - "args": [ "-NoProfile", "-Command" ] + "args": [ + "-NoProfile", + "-Command" + ] } } }, @@ -24,23 +31,31 @@ "options": { "shell": { "executable": "/usr/local/bin/pwsh", - "args": [ "-NoProfile", "-Command" ] + "args": [ + "-NoProfile", + "-Command" + ] } } }, - // Associate with test task runner "tasks": [ + { + "label": "Bootstrap", + "type": "shell", + "command": "./build.ps1 -Task Init -Bootstrap", + "problemMatcher": [] + }, { "label": "Clean", "type": "shell", - "command": "Invoke-psake build.psake.ps1 -taskList Clean", + "command": "./build.ps1 -Task Clean", "problemMatcher": [] }, { "label": "Build", "type": "shell", - "command": "Invoke-psake build.psake.ps1 -taskList Build", + "command": "./build.ps1 -Task Build", "group": { "kind": "build", "isDefault": true @@ -50,36 +65,38 @@ { "label": "BuildHelp", "type": "shell", - "command": "Invoke-psake build.psake.ps1 -taskList BuildHelp", + "command": "./build.ps1 -Task BuildHelp", "problemMatcher": [] }, { "label": "Analyze", "type": "shell", - "command": "Invoke-psake build.psake.ps1 -taskList Analyze", + "command": "./build.ps1 -Task Analyze", "problemMatcher": [] }, { "label": "Install", "type": "shell", - "command": "Invoke-psake build.psake.ps1 -taskList Install", + "command": "./build.ps1 -Task Install", "problemMatcher": [] }, { "label": "Publish", "type": "shell", - "command": "Invoke-psake build.psake.ps1 -taskList Publish", + "command": "./build.ps1 -Task Publish", "problemMatcher": [] }, { "label": "Test", "type": "shell", - "command": "Invoke-Pester -PesterOption @{IncludeVSCodeMarker=$true}", + "command": "./build.ps1 -Task Test", "group": { "kind": "test", "isDefault": true }, - "problemMatcher": [ "$pester" ] + "problemMatcher": [ + "$pester" + ] } - ] -} + ] +} \ No newline at end of file diff --git a/Plaster/Plaster.psd1 b/Plaster/Plaster.psd1 index db48ce5..e0e5ce7 100644 --- a/Plaster/Plaster.psd1 +++ b/Plaster/Plaster.psd1 @@ -1,22 +1,22 @@ @{ # Script module or binary module file associated with this manifest. - RootModule = 'Plaster.psm1' + RootModule = 'Plaster.psm1' # ID used to uniquely identify this module - GUID = 'cfce3c5e-402f-412a-a83a-7b7ee9832ff4' + GUID = 'cfce3c5e-402f-412a-a83a-7b7ee9832ff4' # Version number of this module. - ModuleVersion = '2.0.0' + ModuleVersion = '2.0.0' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a # PSData hashtable with additional module metadata used by PowerShell. - PrivateData = @{ + PrivateData = @{ PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('Plaster', 'CodeGenerator', 'Scaffold', 'Template', 'JSON', 'PowerShell7') + Tags = @('Plaster', 'CodeGenerator', 'Scaffold', 'Template', 'JSON', 'PowerShell7') # A URL to the license for this module. LicenseUri = 'https://github.com/PowerShellOrg/Plaster/blob/master/LICENSE' @@ -61,16 +61,16 @@ For the complete changelog, see: https://github.com/PowerShellOrg/Plaster/blob/m } # Author of this module - Author = 'PowerShell.org' + Author = 'PowerShell.org' # Company or vendor of this module - CompanyName = 'PowerShell.org' + CompanyName = 'PowerShell.org' # Copyright statement for this module - Copyright = '(c) PowerShell.org 2016-2025. All rights reserved.' + Copyright = '(c) PowerShell.org 2016-2025. All rights reserved.' # Description of the functionality provided by this module - Description = 'Plaster is a template-based file and project generator written in PowerShell. Create consistent PowerShell projects with customizable templates supporting both XML and JSON formats.' + Description = 'Plaster is a template-based file and project generator written in PowerShell. Create consistent PowerShell projects with customizable templates supporting both XML and JSON formats.' # Minimum version of the Windows PowerShell engine required by this module PowerShellVersion = '5.1' @@ -99,21 +99,16 @@ For the complete changelog, see: https://github.com/PowerShellOrg/Plaster/blob/m # Functions to export from this module - explicitly list each function that should be # exported. This improves performance of PowerShell when discovering the commands in # module. - FunctionsToExport = @( - 'Invoke-Plaster' - 'New-PlasterManifest' - 'Get-PlasterTemplate' - 'Test-PlasterManifest' - ) + FunctionsToExport = '*' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @() + CmdletsToExport = '*' # Variables to export from this module - # VariablesToExport = @() + VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = @() + AliasesToExport = @() # DSC resources to export from this module # DscResourcesToExport = @() diff --git a/Plaster/Plaster.psm1 b/Plaster/Plaster.psm1 index f254ed5..0e5aafd 100644 --- a/Plaster/Plaster.psm1 +++ b/Plaster/Plaster.psm1 @@ -1,10 +1,3 @@ -#Requires -Version 5.1 - -using namespace System.Management.Automation - -# Module initialization -$ErrorActionPreference = 'Stop' -$InformationPreference = 'Continue' # Import localized data data LocalizedData { @@ -77,7 +70,7 @@ data LocalizedData { # Import localized data with improved error handling try { - Microsoft.PowerShell.Utility\Import-LocalizedData LocalizedData -FileName Plaster.Resources.psd1 -ErrorAction SilentlyContinue + Microsoft.PowerShell.Utility\Import-LocalizedData LocalizedData -FileName 'Plaster.Resources.psd1' -ErrorAction SilentlyContinue } catch { Write-Warning "Failed to import localized data: $_" } @@ -143,82 +136,6 @@ if (-not $script:XmlSchemaValidationSupported) { # Module logging configuration $script:LogLevel = if ($env:PLASTER_LOG_LEVEL) { $env:PLASTER_LOG_LEVEL } else { 'Information' } -function Write-PlasterLog { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')] - [string]$Level, - - [Parameter(Mandatory)] - [string]$Message, - - [string]$Source = 'Plaster' - ) - - $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' - $logMessage = "[$timestamp] [$Level] [$Source] $Message" - - switch ($Level) { - 'Error' { Write-Error $logMessage } - 'Warning' { Write-Warning $logMessage } - 'Information' { Write-Information $logMessage } - 'Verbose' { Write-Verbose $logMessage } - 'Debug' { Write-Debug $logMessage } - } -} - -# Enhanced error handling wrapper -function Invoke-PlasterOperation { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [scriptblock]$ScriptBlock, - - [string]$OperationName = 'PlasterOperation', - - [switch]$PassThru - ) - - try { - Write-PlasterLog -Level Debug -Message "Starting operation: $OperationName" - $result = & $ScriptBlock - Write-PlasterLog -Level Debug -Message "Completed operation: $OperationName" - - if ($PassThru) { - return $result - } - } catch { - $errorMessage = "Operation '$OperationName' failed: $($_.Exception.Message)" - Write-PlasterLog -Level Error -Message $errorMessage - throw $_ - } -} - -# Dot source the individual module command scripts with error handling -$commandFiles = @( - 'NewPlasterManifest.ps1' - 'TestPlasterManifest.ps1' - 'GetPlasterTemplate.ps1' - 'InvokePlaster.ps1' -) - -foreach ($file in $commandFiles) { - $filePath = Join-Path $PSScriptRoot $file - if (Test-Path $filePath) { - try { - Write-PlasterLog -Level Debug -Message "Loading command file: $file" - . $filePath - } catch { - $errorMessage = "Failed to load command file '$file': $($_.Exception.Message)" - Write-PlasterLog -Level Error -Message $errorMessage - throw $_ - } - } else { - Write-PlasterLog -Level Warning -Message "Command file not found: $filePath" - } -} - # Module cleanup on removal $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { Write-PlasterLog -Level Information -Message "Plaster module is being removed" @@ -231,13 +148,5 @@ $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { Remove-Variable -Name 'ParameterDefaultValueStoreRootPath' -Scope Script -ErrorAction SilentlyContinue } -# Export module members explicitly for better performance -Export-ModuleMember -Function @( - 'Invoke-Plaster' - 'New-PlasterManifest' - 'Get-PlasterTemplate' - 'Test-PlasterManifest' -) - # Module initialization complete Write-PlasterLog -Level Information -Message "Plaster v$PlasterVersion module loaded successfully (PowerShell $($PSVersionTable.PSVersion))" \ No newline at end of file diff --git a/Plaster/Private/GetPlasterManifestPathForCulture.ps1 b/Plaster/Private/GetPlasterManifestPathForCulture.ps1 new file mode 100644 index 0000000..1a5625a --- /dev/null +++ b/Plaster/Private/GetPlasterManifestPathForCulture.ps1 @@ -0,0 +1,38 @@ +function GetPlasterManifestPathForCulture { + [CmdletBinding()] + param ( + [string] + $TemplatePath, + [ValidateNotNull()] + [CultureInfo] + $Culture + ) + if (![System.IO.Path]::IsPathRooted($TemplatePath)) { + $TemplatePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($TemplatePath) + } + + # Check for culture-locale first. + $plasterManifestBasename = "plasterManifest" + $plasterManifestFilename = "${plasterManifestBasename}_$($culture.Name).xml" + $plasterManifestPath = Join-Path $TemplatePath $plasterManifestFilename + if (Test-Path $plasterManifestPath) { + return $plasterManifestPath + } + + # Check for culture next. + if ($culture.Parent.Name) { + $plasterManifestFilename = "${plasterManifestBasename}_$($culture.Parent.Name).xml" + $plasterManifestPath = Join-Path $TemplatePath $plasterManifestFilename + if (Test-Path $plasterManifestPath) { + return $plasterManifestPath + } + } + + # Fallback to invariant culture manifest. + $plasterManifestPath = Join-Path $TemplatePath "${plasterManifestBasename}.xml" + if (Test-Path $plasterManifestPath) { + return $plasterManifestPath + } + + $null +} \ No newline at end of file diff --git a/Plaster/Private/InitializePredefinedVariables.ps1 b/Plaster/Private/InitializePredefinedVariables.ps1 new file mode 100644 index 0000000..25c248f --- /dev/null +++ b/Plaster/Private/InitializePredefinedVariables.ps1 @@ -0,0 +1,31 @@ +function InitializePredefinedVariables { + [CmdletBinding()] + param( + [string] + $TemplatePath, + [string] + $DestPath + ) + # Always set these variables, even if the command has been run with -WhatIf + $WhatIfPreference = $false + + Set-Variable -Name PLASTER_TemplatePath -Value $TemplatePath.TrimEnd('\', '/') -Scope Script + + $destName = Split-Path -Path $DestPath -Leaf + Set-Variable -Name PLASTER_DestinationPath -Value $DestPath.TrimEnd('\', '/') -Scope Script + Set-Variable -Name PLASTER_DestinationName -Value $destName -Scope Script + Set-Variable -Name PLASTER_DirSepChar -Value ([System.IO.Path]::DirectorySeparatorChar) -Scope Script + Set-Variable -Name PLASTER_HostName -Value $Host.Name -Scope Script + Set-Variable -Name PLASTER_Version -Value $MyInvocation.MyCommand.Module.Version -Scope Script + + Set-Variable -Name PLASTER_Guid1 -Value ([Guid]::NewGuid()) -Scope Script + Set-Variable -Name PLASTER_Guid2 -Value ([Guid]::NewGuid()) -Scope Script + Set-Variable -Name PLASTER_Guid3 -Value ([Guid]::NewGuid()) -Scope Script + Set-Variable -Name PLASTER_Guid4 -Value ([Guid]::NewGuid()) -Scope Script + Set-Variable -Name PLASTER_Guid5 -Value ([Guid]::NewGuid()) -Scope Script + + $now = [DateTime]::Now + Set-Variable -Name PLASTER_Date -Value ($now.ToShortDateString()) -Scope Script + Set-Variable -Name PLASTER_Time -Value ($now.ToShortTimeString()) -Scope Script + Set-Variable -Name PLASTER_Year -Value ($now.Year) -Scope Script +} \ No newline at end of file diff --git a/Plaster/Private/Invoke-PlasterOperation.ps1 b/Plaster/Private/Invoke-PlasterOperation.ps1 new file mode 100644 index 0000000..4219e05 --- /dev/null +++ b/Plaster/Private/Invoke-PlasterOperation.ps1 @@ -0,0 +1,26 @@ +# Enhanced error handling wrapper +function Invoke-PlasterOperation { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [scriptblock]$ScriptBlock, + + [string]$OperationName = 'PlasterOperation', + + [switch]$PassThru + ) + + try { + Write-PlasterLog -Level Debug -Message "Starting operation: $OperationName" + $result = & $ScriptBlock + Write-PlasterLog -Level Debug -Message "Completed operation: $OperationName" + + if ($PassThru) { + return $result + } + } catch { + $errorMessage = "Operation '$OperationName' failed: $($_.Exception.Message)" + Write-PlasterLog -Level Error -Message $errorMessage + throw $_ + } +} \ No newline at end of file diff --git a/Plaster/Private/Write-PlasterLog.ps1 b/Plaster/Private/Write-PlasterLog.ps1 new file mode 100644 index 0000000..b71631c --- /dev/null +++ b/Plaster/Private/Write-PlasterLog.ps1 @@ -0,0 +1,24 @@ +function Write-PlasterLog { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')] + [string]$Level, + + [Parameter(Mandatory)] + [string]$Message, + + [string]$Source = 'Plaster' + ) + + $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $logMessage = "[$timestamp] [$Level] [$Source] $Message" + + switch ($Level) { + 'Error' { Write-Error $logMessage } + 'Warning' { Write-Warning $logMessage } + 'Information' { Write-Information $logMessage } + 'Verbose' { Write-Verbose $logMessage } + 'Debug' { Write-Debug $logMessage } + } +} \ No newline at end of file diff --git a/Plaster/GetModuleExtension.ps1 b/Plaster/Public/Get-ModuleExtension.ps1 similarity index 84% rename from Plaster/GetModuleExtension.ps1 rename to Plaster/Public/Get-ModuleExtension.ps1 index 29caedd..3f2d410 100644 --- a/Plaster/GetModuleExtension.ps1 +++ b/Plaster/Public/Get-ModuleExtension.ps1 @@ -15,15 +15,15 @@ function Get-ModuleExtension { $modules = Get-Module -ListAvailable if (!$ListAvailable) { $modules = $modules | - Group-Object Name | - Foreach-Object { - $_.group | - Sort-Object Version | - Select-Object -Last 1 - } + Group-Object Name | + ForEach-Object { + $_.group | + Sort-Object Version | + Select-Object -Last 1 + } } - Write-Verbose "`nFound $($modules.Length) installed modules to scan for extensions." + Write-Verbose "Found $($modules.Length) installed modules to scan for extensions." function ParseVersion($versionString) { $parsedVersion = $null @@ -32,7 +32,7 @@ function Get-ModuleExtension { # We're targeting Semantic Versioning 2.0 so make sure the version has # at least 3 components (X.X.X). This logic ensures that the "patch" # (third) component has been specified. - $versionParts = $versionString.Split('.'); + $versionParts = $versionString.Split('.') if ($versionParts.Length -lt 3) { $versionString = "$versionString.0" } @@ -66,10 +66,10 @@ function Get-ModuleExtension { (!$maximumVersion -or $ModuleVersion -le $maximumVersion)) { # Return a new object with the extension information [PSCustomObject]@{ - Module = $module + Module = $module MinimumVersion = $minimumVersion MaximumVersion = $maximumVersion - Details = $extension.Details + Details = $extension.Details } } } diff --git a/Plaster/GetPlasterTemplate.ps1 b/Plaster/Public/Get-PlasterTemplate.ps1 similarity index 88% rename from Plaster/GetPlasterTemplate.ps1 rename to Plaster/Public/Get-PlasterTemplate.ps1 index 3f8ebea..9452f97 100644 --- a/Plaster/GetPlasterTemplate.ps1 +++ b/Plaster/Public/Get-PlasterTemplate.ps1 @@ -1,5 +1,3 @@ -. $PSScriptRoot\GetModuleExtension.ps1 - function Get-PlasterTemplate { [CmdletBinding()] param( @@ -54,18 +52,18 @@ function Get-PlasterTemplate { $metadata = $manifestXml["plasterManifest"]["metadata"] $manifestObj = [PSCustomObject]@{ - Name = $metadata["name"].InnerText - Title = $metadata["title"].InnerText - Author = $metadata["author"].InnerText - Version = New-Object -TypeName "System.Version" -ArgumentList $metadata["version"].InnerText - Description = $metadata["description"].InnerText - Tags = $metadata["tags"].InnerText.split(",") | ForEach-Object { $_.Trim() } + Name = $metadata["name"].InnerText + Title = $metadata["title"].InnerText + Author = $metadata["author"].InnerText + Version = New-Object -TypeName "System.Version" -ArgumentList $metadata["version"].InnerText + Description = $metadata["description"].InnerText + Tags = $metadata["tags"].InnerText.split(",") | ForEach-Object { $_.Trim() } TemplatePath = $manifestPath.Directory.FullName } $manifestObj.PSTypeNames.Insert(0, "Microsoft.PowerShell.Plaster.PlasterTemplate") Add-Member -MemberType ScriptMethod -InputObject $manifestObj -Name "InvokePlaster" -Value { Invoke-Plaster -TemplatePath $this.TemplatePath } - return $manifestObj | Where-Object Name -like $name | Where-Object Tags -like $tag + return $manifestObj | Where-Object Name -Like $name | Where-Object Tags -Like $tag } function GetManifestsUnderPath([string]$rootPath, [bool]$recurse, [string]$name, [string]$tag) { @@ -96,7 +94,7 @@ function Get-PlasterTemplate { if ($IncludeInstalledModules.IsPresent) { # Search for templates in module path $GetModuleExtensionParams = @{ - ModuleName = "Plaster" + ModuleName = "Plaster" ModuleVersion = $PlasterVersion ListAvailable = $ListAvailable } diff --git a/Plaster/InvokePlaster.ps1 b/Plaster/Public/Invoke-Plaster.ps1 similarity index 84% rename from Plaster/InvokePlaster.ps1 rename to Plaster/Public/Invoke-Plaster.ps1 index ca38dbb..f233c7c 100644 --- a/Plaster/InvokePlaster.ps1 +++ b/Plaster/Public/Invoke-Plaster.ps1 @@ -1,3 +1,4 @@ +## TODO: Create tests to ensure check for these. ## DEVELOPERS NOTES & CONVENTIONS ## ## 1. All text displayed to the user except for Write-Debug (or $PSCmdlet.WriteDebug()) text must be added to the @@ -14,14 +15,14 @@ ## 4. Please follow the scripting style of this file when adding new script. function Invoke-Plaster { - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidShouldContinueWithoutForce', '', Scope='Function', Target='CopyFileWithConflictDetection')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope='Function', Target='ProcessParameter')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope='Function', Target='CopyFileWithConflictDetection')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope='Function', Target='ProcessFile')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope='Function', Target='ProcessModifyFile')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope='Function', Target='ProcessNewModuleManifest')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope='Function', Target='ProcessRequireModule')] - [CmdletBinding(SupportsShouldProcess=$true)] + [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidShouldContinueWithoutForce', '', Scope = 'Function', Target = 'CopyFileWithConflictDetection')] + [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope = 'Function', Target = 'ProcessParameter')] + [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'CopyFileWithConflictDetection')] + [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'ProcessFile')] + [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'ProcessModifyFile')] + [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'ProcessNewModuleManifest')] + [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'ProcessRequireModule')] + [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Position = 0, Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -104,7 +105,7 @@ function Invoke-Plaster { switch -regex ($type) { 'text|user-fullname|user-email' { $param = New-Object System.Management.Automation.RuntimeDefinedParameter ` - -ArgumentList ($name, [string], $attributeCollection) + -ArgumentList ($name, [string], $attributeCollection) break } @@ -113,7 +114,7 @@ function Invoke-Plaster { $setValues = New-Object string[] $choiceNodes.Count $i = 0 - foreach ($choiceNode in $choiceNodes){ + foreach ($choiceNode in $choiceNodes) { $setValues[$i++] = $choiceNode.value } @@ -121,17 +122,16 @@ function Invoke-Plaster { $attributeCollection.Add($validateSetAttr) $type = if ($type -eq 'multichoice') { [string[]] } else { [string] } $param = New-Object System.Management.Automation.RuntimeDefinedParameter ` - -ArgumentList ($name, $type, $attributeCollection) + -ArgumentList ($name, $type, $attributeCollection) break } - default { throw ($LocalizedData.UnrecognizedParameterType_F2 -f $type,$name) } + default { throw ($LocalizedData.UnrecognizedParameterType_F2 -f $type, $name) } } $paramDictionary.Add($name, $param) } - } - catch { + } catch { Write-Warning ($LocalizedData.ErrorProcessingDynamicParams_F1 -f $_) } @@ -181,8 +181,7 @@ function Invoke-Plaster { if (Test-Path -LiteralPath $manifestPath -PathType Leaf) { $manifest = Test-PlasterManifest -Path $manifestPath -ErrorAction Stop 3>$null $PSCmdlet.WriteDebug("In begin, loading manifest file '$manifestPath'") - } - else { + } else { throw ($LocalizedData.ManifestFileMissing_F1 -f $manifestPath) } } @@ -199,7 +198,7 @@ function Invoke-Plaster { TemplatePath = $templateAbsolutePath DestinationPath = $destinationAbsolutePath Success = $false - TemplateType = if ($manifest.plasterManifest.templateType) {$manifest.plasterManifest.templateType} else {'Unspecified'} + TemplateType = if ($manifest.plasterManifest.templateType) { $manifest.plasterManifest.templateType } else { 'Unspecified' } CreatedFiles = [string[]]@() UpdatedFiles = [string[]]@() MissingModules = [string[]]@() @@ -220,8 +219,7 @@ function Invoke-Plaster { try { $PSCmdlet.WriteDebug("Loading default value store from '$defaultValueStorePath'.") $defaultValueStore = Import-Clixml $defaultValueStorePath -ErrorAction Stop - } - catch { + } catch { Write-Warning ($LocalizedData.ErrorFailedToLoadStoreFile_F1 -f $defaultValueStorePath) } } @@ -234,44 +232,44 @@ function Invoke-Plaster { $iss.LanguageMode = [System.Management.Automation.PSLanguageMode]::ConstrainedLanguage $iss.DisableFormatUpdates = $true - $sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'Environment',([Microsoft.PowerShell.Commands.EnvironmentProvider]),$null + $sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'Environment', ([Microsoft.PowerShell.Commands.EnvironmentProvider]), $null $iss.Providers.Add($sspe) - $sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'FileSystem',([Microsoft.PowerShell.Commands.FileSystemProvider]),$null + $sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'FileSystem', ([Microsoft.PowerShell.Commands.FileSystemProvider]), $null $iss.Providers.Add($sspe) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Content',([Microsoft.PowerShell.Commands.GetContentCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Content', ([Microsoft.PowerShell.Commands.GetContentCommand]), $null $iss.Commands.Add($ssce) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Date',([Microsoft.PowerShell.Commands.GetDateCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Date', ([Microsoft.PowerShell.Commands.GetDateCommand]), $null $iss.Commands.Add($ssce) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ChildItem',([Microsoft.PowerShell.Commands.GetChildItemCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ChildItem', ([Microsoft.PowerShell.Commands.GetChildItemCommand]), $null $iss.Commands.Add($ssce) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Item',([Microsoft.PowerShell.Commands.GetItemCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Item', ([Microsoft.PowerShell.Commands.GetItemCommand]), $null $iss.Commands.Add($ssce) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ItemProperty',([Microsoft.PowerShell.Commands.GetItemPropertyCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ItemProperty', ([Microsoft.PowerShell.Commands.GetItemPropertyCommand]), $null $iss.Commands.Add($ssce) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Module',([Microsoft.PowerShell.Commands.GetModuleCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Module', ([Microsoft.PowerShell.Commands.GetModuleCommand]), $null $iss.Commands.Add($ssce) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Variable',([Microsoft.PowerShell.Commands.GetVariableCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Variable', ([Microsoft.PowerShell.Commands.GetVariableCommand]), $null $iss.Commands.Add($ssce) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Test-Path',([Microsoft.PowerShell.Commands.TestPathCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Test-Path', ([Microsoft.PowerShell.Commands.TestPathCommand]), $null $iss.Commands.Add($ssce) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Out-String',([Microsoft.PowerShell.Commands.OutStringCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Out-String', ([Microsoft.PowerShell.Commands.OutStringCommand]), $null $iss.Commands.Add($ssce) - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Compare-Object',([Microsoft.PowerShell.Commands.CompareObjectCommand]),$null + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Compare-Object', ([Microsoft.PowerShell.Commands.CompareObjectCommand]), $null $iss.Commands.Add($ssce) $scopedItemOptions = [System.Management.Automation.ScopedItemOptions]::AllScope - $plasterVars = Get-Variable -Name PLASTER_*,PSVersionTable + $plasterVars = Get-Variable -Name PLASTER_*, PSVersionTable if (Test-Path Variable:\IsLinux) { $plasterVars += Get-Variable -Name IsLinux } @@ -286,7 +284,7 @@ function Invoke-Plaster { } foreach ($var in $plasterVars) { $ssve = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry ` - $var.Name,$var.Value,$var.Description,$scopedItemOptions + $var.Name, $var.Value, $var.Description, $scopedItemOptions $iss.Variables.Add($ssve) } @@ -314,18 +312,16 @@ function Invoke-Plaster { $powershell.AddScript($Expression) > $null $res = $powershell.Invoke() $res - } - catch { - throw ($LocalizedData.ExpressionInvalid_F2 -f $Expression,$_) + } catch { + throw ($LocalizedData.ExpressionInvalid_F2 -f $Expression, $_) } # Check for non-terminating errors. if ($powershell.Streams.Error.Count -gt 0) { $err = $powershell.Streams.Error[0] - throw ($LocalizedData.ExpressionNonTermErrors_F2 -f $Expression,$err) + throw ($LocalizedData.ExpressionNonTermErrors_F2 -f $Expression, $err) } - } - finally { + } finally { if ($powershell) { $powershell.Dispose() } @@ -335,73 +331,65 @@ function Invoke-Plaster { function InterpolateAttributeValue([string]$Value, [string]$Location) { if ($null -eq $Value) { return [string]::Empty - } - elseif ([string]::IsNullOrWhiteSpace($Value)) { + } elseif ([string]::IsNullOrWhiteSpace($Value)) { return $Value } try { $res = @(ExecuteExpressionImpl "`"$Value`"") [string]$res[0] - } - catch { - throw ($LocalizedData.InterpolationError_F3 -f $Value.Trim(),$Location,$_) + } catch { + throw ($LocalizedData.InterpolationError_F3 -f $Value.Trim(), $Location, $_) } } function EvaluateConditionAttribute([string]$Expression, [string]$Location) { if ($null -eq $Expression) { return [string]::Empty - } - elseif ([string]::IsNullOrWhiteSpace($Expression)) { + } elseif ([string]::IsNullOrWhiteSpace($Expression)) { return $Expression } try { $res = @(ExecuteExpressionImpl $Expression) [bool]$res[0] - } - catch { - throw ($LocalizedData.ExpressionInvalidCondition_F3 -f $Expression,$Location,$_) + } catch { + throw ($LocalizedData.ExpressionInvalidCondition_F3 -f $Expression, $Location, $_) } } function EvaluateExpression([string]$Expression, [string]$Location) { if ($null -eq $Expression) { return [string]::Empty - } - elseif ([string]::IsNullOrWhiteSpace($Expression)) { + } elseif ([string]::IsNullOrWhiteSpace($Expression)) { return $Expression } try { $res = @(ExecuteExpressionImpl $Expression) [string]$res[0] - } - catch { - throw ($LocalizedData.ExpressionExecError_F2 -f $Location,$_) + } catch { + throw ($LocalizedData.ExpressionExecError_F2 -f $Location, $_) } } function EvaluateScript([string]$Script, [string]$Location) { if ($null -eq $Script) { return @([string]::Empty) - } - elseif ([string]::IsNullOrWhiteSpace($Script)) { + } elseif ([string]::IsNullOrWhiteSpace($Script)) { return $Script } try { $res = @(ExecuteExpressionImpl $Script) [string[]]$res - } - catch { - throw ($LocalizedData.ExpressionExecError_F2 -f $Location,$_) + } catch { + throw ($LocalizedData.ExpressionExecError_F2 -f $Location, $_) } } function GetErrorLocationFileAttrVal([string]$ElementName, [string]$AttributeName) { - $LocalizedData.ExpressionErrorLocationFile_F2 -f $ElementName,$AttributeName + $LocalizedData.ExpressionErrorLocationFile_F2 -f $ElementName, $AttributeName } function GetErrorLocationModifyAttrVal([string]$AttributeName) { @@ -413,11 +401,11 @@ function Invoke-Plaster { } function GetErrorLocationParameterAttrVal([string]$ParameterName, [string]$AttributeName) { - $LocalizedData.ExpressionErrorLocationParameter_F2 -f $ParameterName,$AttributeName + $LocalizedData.ExpressionErrorLocationParameter_F2 -f $ParameterName, $AttributeName } function GetErrorLocationRequireModuleAttrVal([string]$ModuleName, [string]$AttributeName) { - $LocalizedData.ExpressionErrorLocationRequireModule_F2 -f $ModuleName,$AttributeName + $LocalizedData.ExpressionErrorLocationRequireModule_F2 -f $ModuleName, $AttributeName } function GetPSSnippetFunction([String]$FilePath) { @@ -440,7 +428,7 @@ function Invoke-Plaster { throw ($LocalizedData.ErrorPathMustBeUnderDestPath_F2 -f $fullPath, $fullDestPath) } - $fullPath.Substring($fullDestPath.Length).TrimStart('\','/') + $fullPath.Substring($fullDestPath.Length).TrimStart('\', '/') } function VerifyPathIsUnderDestinationPath([ValidateNotNullOrEmpty()][string]$FullPath) { @@ -460,7 +448,7 @@ function Invoke-Plaster { function WriteContentWithEncoding([string]$path, [string[]]$content, [string]$encoding) { if ($encoding -match '-nobom') { - $encoding,$dummy = $encoding -split '-' + $encoding, $dummy = $encoding -split '-' $noBomEncoding = $null switch ($encoding) { @@ -472,8 +460,7 @@ function Invoke-Plaster { } [System.IO.File]::WriteAllLines($path, $content, $noBomEncoding) - } - else { + } else { Set-Content -LiteralPath $path -Value $content -Encoding $encoding } } @@ -493,11 +480,11 @@ function Invoke-Plaster { } function GetMaxOperationLabelLength { - ($LocalizedData.OpCreate, $LocalizedData.OpIdentical, - $LocalizedData.OpConflict, $LocalizedData.OpForce, - $LocalizedData.OpMissing, $LocalizedData.OpModify, - $LocalizedData.OpUpdate, $LocalizedData.OpVerify | - Measure-Object -Property Length -Maximum).Maximum + ($LocalizedData.OpCreate, $LocalizedData.OpIdentical, + $LocalizedData.OpConflict, $LocalizedData.OpForce, + $LocalizedData.OpMissing, $LocalizedData.OpModify, + $LocalizedData.OpUpdate, $LocalizedData.OpVerify | + Measure-Object -Property Length -Maximum).Maximum } function WriteOperationStatus($operation, $message) { @@ -511,7 +498,7 @@ function Invoke-Plaster { foreach ($msg in $Message) { $lines = $msg -split "`n" foreach ($line in $lines) { - Write-Host ("{0,$maxLen} {1}" -f "",$line) + Write-Host ("{0,$maxLen} {1}" -f "", $line) } } } @@ -531,8 +518,7 @@ function Invoke-Plaster { if (Test-Path -LiteralPath $gitConfigPath) { $matches = Select-String -LiteralPath $gitConfigPath -Pattern "\s+$name\s+=\s+(.+)$" - if (@($matches).Count -gt 0) - { + if (@($matches).Count -gt 0) { $matches.Matches.Groups[1].Value } } @@ -548,12 +534,10 @@ function Invoke-Plaster { if (!$value -and $default) { $value = $default $patternMatch = $true - } - elseif ($value -and $pattern) { + } elseif ($value -and $pattern) { if ($value -match $pattern) { $patternMatch = $true - } - else { + } else { $PSCmdlet.WriteDebug("Value '$value' did not match the pattern '$pattern'") } } @@ -563,22 +547,22 @@ function Invoke-Plaster { } function PromptForChoice([string]$ParameterName, [ValidateNotNull()]$ChoiceNodes, [string]$prompt, - [int[]]$defaults, [switch]$IsMultiChoice) { + [int[]]$defaults, [switch]$IsMultiChoice) { $choices = New-Object 'System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription]' $values = New-Object object[] $ChoiceNodes.Count $i = 0 foreach ($choiceNode in $ChoiceNodes) { $label = InterpolateAttributeValue $choiceNode.label (GetErrorLocationParameterAttrVal $ParameterName label) - $help = InterpolateAttributeValue $choiceNode.help (GetErrorLocationParameterAttrVal $ParameterName help) + $help = InterpolateAttributeValue $choiceNode.help (GetErrorLocationParameterAttrVal $ParameterName help) $value = InterpolateAttributeValue $choiceNode.value (GetErrorLocationParameterAttrVal $ParameterName value) - $choice = New-Object System.Management.Automation.Host.ChoiceDescription -Arg $label,$help + $choice = New-Object System.Management.Automation.Host.ChoiceDescription -Arg $label, $help $choices.Add($choice) $values[$i++] = $value } - $retval = [PSCustomObject]@{Values=@(); Indices=@()} + $retval = [PSCustomObject]@{Values = @(); Indices = @() } if ($IsMultiChoice) { $selections = $Host.UI.PromptForChoice('', $prompt, $choices, $defaults) @@ -586,8 +570,7 @@ function Invoke-Plaster { $retval.Values += $values[$selection] $retval.Indices += $selection } - } - else { + } else { if ($defaults.Count -gt 1) { throw ($LocalizedData.ParameterTypeChoiceMultipleDefault_F1 -f $ChoiceNodes.ParentNode.name) } @@ -606,11 +589,11 @@ function Invoke-Plaster { # the latest Plaster variables. function SetPlasterVariable() { param( - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Name, - [Parameter(Mandatory=$true)] + [Parameter(Mandatory = $true)] [AllowNull()] $Value, @@ -648,8 +631,7 @@ function Invoke-Plaster { if (-not [string]::IsNullOrEmpty($default) -and $type -eq 'text') { SetPlasterVariable -Name $name -Value $default -IsParam $true $PSCmdlet.WriteDebug("The condition of the parameter $($name) with the type 'text' evaluated to false. The parameter has a default value which will be used.") - } - else { + } else { # Define the parameter so later conditions can use it but its value will be $null SetPlasterVariable -Name $name -Value $null -IsParam $true $PSCmdlet.WriteDebug("Skipping parameter $($name), condition evaluated to false.") @@ -663,8 +645,7 @@ function Invoke-Plaster { # Check if parameter was provided via a dynamic parameter. if ($boundParameters.ContainsKey($name)) { $value = $boundParameters[$name] - } - else { + } else { # Not a dynamic parameter so prompt user for the value but first check for a stored default value. if ($store -and ($null -ne $defaultValueStore[$name])) { $default = $defaultValueStore[$name] @@ -672,11 +653,10 @@ function Invoke-Plaster { if (($store -eq 'encrypted') -and ($default -is [System.Security.SecureString])) { try { - $cred = New-Object -TypeName PSCredential -ArgumentList 'jsbplh',$default + $cred = New-Object -TypeName PSCredential -ArgumentList 'jsbplh', $default $default = $cred.GetNetworkCredential().Password $PSCmdlet.WriteDebug("Unencrypted default value for parameter '$name'.") - } - catch [System.Exception] { + } catch [System.Exception] { Write-Warning ($LocalizedData.ErrorUnencryptingSecureString_F1 -f $name) } } @@ -704,8 +684,7 @@ function Invoke-Plaster { if ($store -eq 'encrypted') { $obscuredDefault = $default -replace '(....).*', '$1****' $prompt += " ($obscuredDefault)" - } - else { + } else { $prompt += " ($default)" } } @@ -724,8 +703,7 @@ function Invoke-Plaster { if ($store -eq 'encrypted') { $obscuredDefault = $default -replace '(....).*', '$1****' $prompt += " ($obscuredDefault)" - } - else { + } else { $prompt += " ($default)" } } @@ -745,8 +723,7 @@ function Invoke-Plaster { if ($store -eq 'encrypted') { $obscuredDefault = $default -replace '(....).*', '$1****' $prompt += " ($obscuredDefault)" - } - else { + } else { $prompt += " ($default)" } } @@ -765,7 +742,7 @@ function Invoke-Plaster { $OFS = "," $valueToStore = "$($selections.Indices)" } - default { throw ($LocalizedData.UnrecognizedParameterType_F2 -f $type, $Node.LocalName) } + default { throw ($LocalizedData.UnrecognizedParameterType_F2 -f $type, $Node.LocalName) } } # If parameter specifies that user's input be stored as the default value, @@ -774,8 +751,7 @@ function Invoke-Plaster { if ($store -eq 'encrypted') { $PSCmdlet.WriteDebug("Storing new, encrypted default value for parameter '$name' to default value store.") $defaultValueStore[$name] = ConvertTo-SecureString -String $valueToStore -AsPlainText -Force - } - else { + } else { $PSCmdlet.WriteDebug("Storing new default value '$valueToStore' for parameter '$name' to default value store.") $defaultValueStore[$name] = $valueToStore } @@ -794,11 +770,11 @@ function Invoke-Plaster { # Eliminate whitespace before and after the text that just happens to get inserted because you want # the text on different lines than the start/end element tags. - $trimmedText = $text -replace '^[ \t]*\n','' -replace '\n[ \t]*$','' + $trimmedText = $text -replace '^[ \t]*\n', '' -replace '\n[ \t]*$', '' - $condition = $Node.condition + $condition = $Node.condition if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)>'")) { - $debugText = $trimmedText -replace '\r|\n',' ' + $debugText = $trimmedText -replace '\r|\n', ' ' $maxLength = [Math]::Min(40, $debugText.Length) $PSCmdlet.WriteDebug("Skipping message '$($debugText.Substring(0, $maxLength))', condition evaluated to false.") return @@ -823,12 +799,12 @@ function Invoke-Plaster { # but I think it is better to let the template author know they've broken the # rules for any of the file directives (not just the ones they're testing/enabled). if ([System.IO.Path]::IsPathRooted($dstRelPath)) { - throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $dstRelPath,$Node.LocalName) + throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $dstRelPath, $Node.LocalName) } $dstPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath((Join-Path $DestinationPath $dstRelPath)) - $condition = $Node.condition + $condition = $Node.condition if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)>'")) { $PSCmdlet.WriteDebug("Skipping module manifest generation for '$dstPath', condition evaluated to false.") return @@ -852,7 +828,7 @@ function Invoke-Plaster { # If there is an existing module manifest, load it so we can reuse old values not specified by # template. if (Test-Path -LiteralPath $dstPath) { - $manifestFileName = Split-Path $dstPath -leaf + $manifestFileName = Split-Path $dstPath -Leaf $newModuleManifestParams = Import-LocalizedData -BaseDirectory $manifestDir -FileName $manifestFileName if ($newModuleManifestParams.PrivateData) { $newModuleManifestParams += $newModuleManifestParams.PrivateData.psdata @@ -913,8 +889,7 @@ function Invoke-Plaster { if ($PassThru -and ($Node.openInEditor -eq 'true')) { $InvokePlasterInfo.OpenFiles += $dstPath } - } - finally { + } finally { if ($tempFile -and (Test-Path $tempFile)) { Remove-Item -LiteralPath $tempFile $PSCmdlet.WriteDebug("Removed temp file for new module manifest - $tempFile") @@ -930,7 +905,7 @@ function Invoke-Plaster { $dir = [System.IO.Path]::GetDirectoryName($Path) $filename = [System.IO.Path]::GetFileName($Path) $backupPath = Join-Path -Path $dir -ChildPath "${filename}.bak" - $i = 1; + $i = 1 while (Test-Path -LiteralPath $backupPath) { $backupPath = Join-Path -Path $dir -ChildPath "${filename}.bak$i" $i++ @@ -954,14 +929,14 @@ function Invoke-Plaster { } function NewFileSystemCopyInfo([string]$srcPath, [string]$dstPath) { - [PSCustomObject]@{SrcFileName=$srcPath; DstFileName=$dstPath} + [PSCustomObject]@{SrcFileName = $srcPath; DstFileName = $dstPath } } function ExpandFileSourceSpec([string]$srcRelPath, [string]$dstRelPath) { $srcPath = Join-Path $templateAbsolutePath $srcRelPath $dstPath = Join-Path $destinationAbsolutePath $dstRelPath - if ($srcRelPath.IndexOfAny([char[]]('*','?')) -lt 0) { + if ($srcRelPath.IndexOfAny([char[]]('*', '?')) -lt 0) { # No wildcard spec in srcRelPath so return info on single file. # Also, if dstRelPath is empty, then use source rel path. if (!$dstRelPath) { @@ -980,9 +955,8 @@ function Invoke-Plaster { if ($leaf -eq '**') { $gciParams['Recurse'] = $true - } - else { - if ($leaf.IndexOfAny([char[]]('*','?')) -ge 0) { + } else { + if ($leaf.IndexOfAny([char[]]('*', '?')) -ge 0) { $gciParams['Filter'] = $leaf } @@ -1009,7 +983,7 @@ function Invoke-Plaster { $gciParams.Remove('File') $gciParams['Directory'] = $true $dirs = @(Microsoft.PowerShell.Management\Get-ChildItem @gciParams | - Where-Object {$_.GetFileSystemInfos().Length -eq 0}) + Where-Object { $_.GetFileSystemInfos().Length -eq 0 }) foreach ($dir in $dirs) { $dirSrcPath = $dir.FullName $relPath = $dirSrcPath.Substring($srcRelRootPathLength) @@ -1042,16 +1016,13 @@ function Invoke-Plaster { if (Test-Path -LiteralPath $DstPath) { if (AreFilesIdentical $SrcPath $DstPath) { $operation = $LocalizedData.OpIdentical - } - elseif ($templateCreatedFiles.ContainsKey($DstPath)) { + } elseif ($templateCreatedFiles.ContainsKey($DstPath)) { # Plaster created this file previously during template invocation # therefore, there is no conflict. We're simply updating the file. $operation = $LocalizedData.OpUpdate - } - elseif ($Force) { + } elseif ($Force) { $operation = $LocalizedData.OpForce - } - else { + } else { $operation = $LocalizedData.OpConflict } } @@ -1071,11 +1042,10 @@ function Invoke-Plaster { $InvokePlasterInfo.CreatedFiles += $DstPath } $templateCreatedFiles[$DstPath] = $null - } - elseif ($Force -or $PSCmdlet.ShouldContinue(($LocalizedData.OverwriteFile_F1 -f $DstPath), - $LocalizedData.FileConflict, - [ref]$fileConflictConfirmYesToAll, - [ref]$fileConflictConfirmNoToAll)) { + } elseif ($Force -or $PSCmdlet.ShouldContinue(($LocalizedData.OverwriteFile_F1 -f $DstPath), + $LocalizedData.FileConflict, + [ref]$fileConflictConfirmYesToAll, + [ref]$fileConflictConfirmNoToAll)) { $backupFilename = NewBackupFilename $DstPath Copy-Item -LiteralPath $DstPath -Destination $backupFilename Copy-Item -LiteralPath $SrcPath -Destination $DstPath @@ -1096,7 +1066,7 @@ function Invoke-Plaster { $srcRelPath = InterpolateAttributeValue $Node.source (GetErrorLocationFileAttrVal $Node.localName source) $dstRelPath = InterpolateAttributeValue $Node.destination (GetErrorLocationFileAttrVal $Node.localName destination) - $condition = $Node.condition + $condition = $Node.condition if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)>'")) { $PSCmdlet.WriteDebug("Skipping $($Node.localName) '$srcRelPath' -> '$dstRelPath', condition evaluated to false.") return @@ -1106,11 +1076,11 @@ function Invoke-Plaster { # The path may not be valid if it evaluates to false depending # on whether or not conditional parameters are used in the template. if ([System.IO.Path]::IsPathRooted($srcRelPath)) { - throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $srcRelPath,$Node.LocalName) + throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $srcRelPath, $Node.LocalName) } if ([System.IO.Path]::IsPathRooted($dstRelPath)) { - throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $dstRelPath,$Node.LocalName) + throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $dstRelPath, $Node.LocalName) } # Check if node is the specialized, node. @@ -1138,7 +1108,7 @@ function Invoke-Plaster { if (!(Test-Path -LiteralPath $dstPath)) { if ($PSCmdlet.ShouldProcess($parentDir, $LocalizedData.ShouldProcessCreateDir)) { WriteOperationStatus $LocalizedData.OpCreate ` - ($dstRelPath.TrimEnd(([char]'\'),([char]'/')) + [System.IO.Path]::DirectorySeparatorChar) + ($dstRelPath.TrimEnd(([char]'\'), ([char]'/')) + [System.IO.Path]::DirectorySeparatorChar) New-Item -Path $dstPath -ItemType Directory > $null } } @@ -1166,29 +1136,28 @@ function Invoke-Plaster { # Eval script expression delimiters if ($content -and ($content.Count -gt 0)) { $newContent = [regex]::Replace($content, '(<%=)(.*?)(%>)', { - param($match) - $expr = $match.groups[2].value - $res = EvaluateExpression $expr "templateFile '$srcRelPath'" - $PSCmdlet.WriteDebug("Replacing '$expr' with '$res' in contents of template file '$srcPath'") - $res - }, @('IgnoreCase')) + param($match) + $expr = $match.groups[2].value + $res = EvaluateExpression $expr "templateFile '$srcRelPath'" + $PSCmdlet.WriteDebug("Replacing '$expr' with '$res' in contents of template file '$srcPath'") + $res + }, @('IgnoreCase')) # Eval script block delimiters $newContent = [regex]::Replace($newContent, '(^<%)(.*?)(^%>)', { - param($match) - $expr = $match.groups[2].value - $res = EvaluateScript $expr "templateFile '$srcRelPath'" - $res = $res -join [System.Environment]::NewLine - $PSCmdlet.WriteDebug("Replacing '$expr' with '$res' in contents of template file '$srcPath'") - $res - }, @('IgnoreCase', 'SingleLine', 'MultiLine')) + param($match) + $expr = $match.groups[2].value + $res = EvaluateScript $expr "templateFile '$srcRelPath'" + $res = $res -join [System.Environment]::NewLine + $PSCmdlet.WriteDebug("Replacing '$expr' with '$res' in contents of template file '$srcPath'") + $res + }, @('IgnoreCase', 'SingleLine', 'MultiLine')) $srcPath = $tempFile = [System.IO.Path]::GetTempFileName() $PSCmdlet.WriteDebug("Created temp file for expanded templateFile - $tempFile") WriteContentWithEncoding -Path $tempFile -Content $newContent -Encoding $encoding - } - else { + } else { $PSCmdlet.WriteDebug("Skipping template file expansion for $($Node.localName) '$srcPath', file is empty.") } } @@ -1198,8 +1167,7 @@ function Invoke-Plaster { if ($PassThru -and ($Node.openInEditor -eq 'true')) { $InvokePlasterInfo.OpenFiles += $dstPath } - } - finally { + } finally { if ($tempFile -and (Test-Path $tempFile)) { Remove-Item -LiteralPath $tempFile $PSCmdlet.WriteDebug("Removed temp file for expanded templateFile - $tempFile") @@ -1215,7 +1183,7 @@ function Invoke-Plaster { # but I think it is better to let the template author know they've broken the # rules for any of the file directives (not just the ones they're testing/enabled). if ([System.IO.Path]::IsPathRooted($path)) { - throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $path,$Node.LocalName) + throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $path, $Node.LocalName) } $filePath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath((Join-Path $DestinationPath $path)) @@ -1255,7 +1223,7 @@ function Invoke-Plaster { switch ($childNode.LocalName) { 'replace' { - $condition = $childNode.condition + $condition = $childNode.condition if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)><$($childNode.LocalName)>'")) { $PSCmdlet.WriteDebug("Skipping $($Node.LocalName) $($childNode.LocalName) of '$filePath', condition evaluated to false.") continue @@ -1263,8 +1231,7 @@ function Invoke-Plaster { if ($childNode.original -is [string]) { $original = $childNode.original - } - else { + } else { $original = $childNode.original.InnerText } @@ -1274,8 +1241,7 @@ function Invoke-Plaster { if ($childNode.substitute -is [string]) { $substitute = $childNode.substitute - } - else { + } else { $substitute = $childNode.substitute.InnerText } @@ -1287,9 +1253,9 @@ function Invoke-Plaster { # Perform Literal Replacement on FileContent (since it will have regex characters) if ($childNode.substitute.isFile) { - $fileContent = $fileContent.Replace($original,$substitute) + $fileContent = $fileContent.Replace($original, $substitute) } else { - $fileContent = $fileContent -replace $original,$substitute + $fileContent = $fileContent -replace $original, $substitute } # Update the Plaster (non-parameter) variable's value in this and the constrained runspace. @@ -1320,12 +1286,10 @@ function Invoke-Plaster { if ($PassThru -and ($Node.openInEditor -eq 'true')) { $InvokePlasterInfo.OpenFiles += $filePath } - } - else { + } else { WriteOperationStatus $LocalizedData.OpIdentical (ConvertToDestinationRelativePath $filePath) } - } - finally { + } finally { if ($tempFile -and (Test-Path $tempFile)) { Remove-Item -LiteralPath $tempFile $PSCmdlet.WriteDebug("Removed temp file for modified file - $tempFile") @@ -1357,11 +1321,10 @@ function Invoke-Plaster { # Also construct an array of version strings that can be displayed to the user. $versionInfo = @() if ($requiredVersion) { - $getModuleParams["FullyQualifiedName"] = @{ModuleName = $name; RequiredVersion = $requiredVersion} + $getModuleParams["FullyQualifiedName"] = @{ModuleName = $name; RequiredVersion = $requiredVersion } $versionInfo += $LocalizedData.RequireModuleRequiredVersion_F1 -f $requiredVersion - } - elseif ($minimumVersion -or $maximumVersion) { - $getModuleParams["FullyQualifiedName"] = @{ModuleName = $name} + } elseif ($minimumVersion -or $maximumVersion) { + $getModuleParams["FullyQualifiedName"] = @{ModuleName = $name } if ($minimumVersion) { $getModuleParams.FullyQualifiedName["ModuleVersion"] = $minimumVersion @@ -1371,8 +1334,7 @@ function Invoke-Plaster { $getModuleParams.FullyQualifiedName["MaximumVersion"] = $maximumVersion $versionInfo += $LocalizedData.RequireModuleMaxVersion_F1 -f $maximumVersion } - } - else { + } else { $getModuleParams["Name"] = $name } @@ -1394,25 +1356,22 @@ function Invoke-Plaster { $moduleDesc = if ($versionRequirements) { "${name}:$versionRequirements" } else { $name } if ($null -eq $module) { - WriteOperationStatus $LocalizedData.OpMissing ($LocalizedData.RequireModuleMissing_F2 -f $name,$versionRequirements) + WriteOperationStatus $LocalizedData.OpMissing ($LocalizedData.RequireModuleMissing_F2 -f $name, $versionRequirements) if ($message) { WriteOperationAdditionalStatus $message } if ($PassThru) { $InvokePlasterInfo.MissingModules += $moduleDesc } - } - else { + } else { if ($PSVersionTable.PSVersion.Major -gt 3) { - WriteOperationStatus $LocalizedData.OpVerify ($LocalizedData.RequireModuleVerified_F2 -f $name,$versionRequirements) - } - else { + WriteOperationStatus $LocalizedData.OpVerify ($LocalizedData.RequireModuleVerified_F2 -f $name, $versionRequirements) + } else { # On V3, we have to the version matching with the results that Get-Module return. - $installedVersion = $module | Sort-Object Version -Descending | Select-Object -First 1 | Foreach-Object Version + $installedVersion = $module | Sort-Object Version -Descending | Select-Object -First 1 | ForEach-Object Version if ($installedVersion.Build -eq -1) { $installedVersion = [System.Version]"${installedVersion}.0.0" - } - elseif ($installedVersion.Revision -eq -1) { + } elseif ($installedVersion.Revision -eq -1) { $installedVersion = [System.Version]"${installedVersion}.0" } @@ -1420,13 +1379,12 @@ function Invoke-Plaster { ($minimumVersion -and ($installedVersion -lt $minimumVersion)) -or ($maximumVersion -and ($installedVersion -gt $maximumVersion))) { - WriteOperationStatus $LocalizedData.OpMissing ($LocalizedData.RequireModuleMissing_F2 -f $name,$versionRequirements) + WriteOperationStatus $LocalizedData.OpMissing ($LocalizedData.RequireModuleMissing_F2 -f $name, $versionRequirements) if ($PassThru) { $InvokePlasterInfo.MissingModules += $moduleDesc } - } - else { - WriteOperationStatus $LocalizedData.OpVerify ($LocalizedData.RequireModuleVerified_F2 -f $name,$versionRequirements) + } else { + WriteOperationStatus $LocalizedData.OpVerify ($LocalizedData.RequireModuleVerified_F2 -f $name, $versionRequirements) } } } @@ -1439,8 +1397,8 @@ function Invoke-Plaster { foreach ($node in $manifest.plasterManifest.parameters.ChildNodes) { if ($node -isnot [System.Xml.XmlElement]) { continue } switch ($node.LocalName) { - 'parameter' { ProcessParameter $node } - default { throw ($LocalizedData.UnrecognizedParametersElement_F1 -f $node.LocalName) } + 'parameter' { ProcessParameter $node } + default { throw ($LocalizedData.UnrecognizedParametersElement_F1 -f $node.LocalName) } } } @@ -1469,11 +1427,11 @@ function Invoke-Plaster { switch -Regex ($node.LocalName) { 'file|templateFile' { ProcessFile $node; break } - 'message' { ProcessMessage $node; break } - 'modify' { ProcessModifyFile $node; break } + 'message' { ProcessMessage $node; break } + 'modify' { ProcessModifyFile $node; break } 'newModuleManifest' { ProcessNewModuleManifest $node; break } - 'requireModule' { ProcessRequireModule $node; break } - default { throw ($LocalizedData.UnrecognizedContentElement_F1 -f $node.LocalName) } + 'requireModule' { ProcessRequireModule $node; break } + default { throw ($LocalizedData.UnrecognizedContentElement_F1 -f $node.LocalName) } } } @@ -1481,8 +1439,7 @@ function Invoke-Plaster { $InvokePlasterInfo.Success = $true $InvokePlasterInfo } - } - finally { + } finally { # Dispose of the ConstrainedRunspace. if ($constrainedRunspace) { $constrainedRunspace.Dispose() @@ -1491,63 +1448,3 @@ function Invoke-Plaster { } } } - -############################################################################### -# Helper functions -############################################################################### - -function InitializePredefinedVariables([string]$TemplatePath, [string]$DestPath) { - # Always set these variables, even if the command has been run with -WhatIf - $WhatIfPreference = $false - - Set-Variable -Name PLASTER_TemplatePath -Value $TemplatePath.TrimEnd('\','/') -Scope Script - - $destName = Split-Path -Path $DestPath -Leaf - Set-Variable -Name PLASTER_DestinationPath -Value $DestPath.TrimEnd('\','/') -Scope Script - Set-Variable -Name PLASTER_DestinationName -Value $destName -Scope Script - Set-Variable -Name PLASTER_DirSepChar -Value ([System.IO.Path]::DirectorySeparatorChar) -Scope Script - Set-Variable -Name PLASTER_HostName -Value $Host.Name -Scope Script - Set-Variable -Name PLASTER_Version -Value $MyInvocation.MyCommand.Module.Version -Scope Script - - Set-Variable -Name PLASTER_Guid1 -Value ([Guid]::NewGuid()) -Scope Script - Set-Variable -Name PLASTER_Guid2 -Value ([Guid]::NewGuid()) -Scope Script - Set-Variable -Name PLASTER_Guid3 -Value ([Guid]::NewGuid()) -Scope Script - Set-Variable -Name PLASTER_Guid4 -Value ([Guid]::NewGuid()) -Scope Script - Set-Variable -Name PLASTER_Guid5 -Value ([Guid]::NewGuid()) -Scope Script - - $now = [DateTime]::Now - Set-Variable -Name PLASTER_Date -Value ($now.ToShortDateString()) -Scope Script - Set-Variable -Name PLASTER_Time -Value ($now.ToShortTimeString()) -Scope Script - Set-Variable -Name PLASTER_Year -Value ($now.Year) -Scope Script -} - -function GetPlasterManifestPathForCulture([string]$TemplatePath, [ValidateNotNull()][CultureInfo]$Culture) { - if (![System.IO.Path]::IsPathRooted($TemplatePath)) { - $TemplatePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($TemplatePath) - } - - # Check for culture-locale first. - $plasterManifestBasename = "plasterManifest" - $plasterManifestFilename = "${plasterManifestBasename}_$($culture.Name).xml" - $plasterManifestPath = Join-Path $TemplatePath $plasterManifestFilename - if (Test-Path $plasterManifestPath) { - return $plasterManifestPath - } - - # Check for culture next. - if ($culture.Parent.Name) { - $plasterManifestFilename = "${plasterManifestBasename}_$($culture.Parent.Name).xml" - $plasterManifestPath = Join-Path $TemplatePath $plasterManifestFilename - if (Test-Path $plasterManifestPath) { - return $plasterManifestPath - } - } - - # Fallback to invariant culture manifest. - $plasterManifestPath = Join-Path $TemplatePath "${plasterManifestBasename}.xml" - if (Test-Path $plasterManifestPath) { - return $plasterManifestPath - } - - $null -} diff --git a/Plaster/NewPlasterManifest.ps1 b/Plaster/Public/New-PlasterManifest.ps1 similarity index 100% rename from Plaster/NewPlasterManifest.ps1 rename to Plaster/Public/New-PlasterManifest.ps1 diff --git a/Plaster/TestPlasterManifest.ps1 b/Plaster/Public/Test-PlasterManifest.ps1 similarity index 100% rename from Plaster/TestPlasterManifest.ps1 rename to Plaster/Public/Test-PlasterManifest.ps1 diff --git a/build.ps1 b/build.ps1 index 57d968b..c408421 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,514 +1,64 @@ -#Requires -Version 5.1 -#Requires -Modules InvokeBuild - -<# -.SYNOPSIS - Modern build script for Plaster 2.0 using InvokeBuild - -.DESCRIPTION - This build script replaces the legacy psake build system with a modern, - cross-platform build process using InvokeBuild. Supports PowerShell 5.1+ - and PowerShell 7.x on Windows, Linux, and macOS. - -.PARAMETER Task - The build task(s) to execute. Default is 'Build'. - -.PARAMETER Configuration - The build configuration (Debug/Release). Default is 'Release'. - -.PARAMETER OutputPath - The output directory for build artifacts. Default is './Output'. - -.PARAMETER ModuleName - The name of the module being built. Default is 'Plaster'. - -.PARAMETER SkipTests - Skip running tests during the build process. - -.PARAMETER SkipAnalysis - Skip running PSScriptAnalyzer during the build process. - -.PARAMETER PublishToGallery - Publish the module to PowerShell Gallery after successful build and test. - -.PARAMETER NuGetApiKey - API key for publishing to PowerShell Gallery. - -.EXAMPLE - ./build.ps1 - Runs the default Build task - -.EXAMPLE - ./build.ps1 -Task Test - Runs only the Test task - -.EXAMPLE - ./build.ps1 -Task Build, Test, Publish -PublishToGallery -NuGetApiKey $apiKey - Builds, tests, and publishes the module -#> - -[CmdletBinding()] +[CmdletBinding(DefaultParameterSetName = 'Task')] param( - [Parameter()] - [ValidateSet('Clean', 'Build', 'Test', 'Analyze', 'Package', 'Publish', 'Install')] - [string[]]$Task = @('Build'), - - [Parameter()] - [ValidateSet('Debug', 'Release')] - [string]$Configuration = 'Release', - - [Parameter()] - [string]$OutputPath = './Output', - - [Parameter()] - [string]$ModuleName = 'Plaster', - - [Parameter()] - [switch]$SkipTests, - - [Parameter()] - [switch]$SkipAnalysis, - - [Parameter()] - [switch]$PublishToGallery, - - [Parameter()] - [string]$NuGetApiKey -) - -# Build configuration -$script:BuildConfig = @{ - ModuleName = $ModuleName - SourcePath = './src' - OutputPath = $OutputPath - TestPath = './tests' - DocsPath = './docs' - Configuration = $Configuration - ModuleVersion = $null # Will be read from manifest - BuildNumber = $env:BUILD_NUMBER ?? '0' - IsCI = $null -ne $env:CI - - # Tool paths - Tools = @{ - Pester = $null - PSScriptAnalyzer = $null - platyPS = $null - } - - # Test configuration - TestConfig = @{ - OutputFormat = 'NUnitXml' - OutputPath = Join-Path $OutputPath 'TestResults.xml' - CodeCoverage = @{ - Enabled = $true - OutputPath = Join-Path $OutputPath 'CodeCoverage.xml' - OutputFormat = 'JaCoCo' - Threshold = 80 - } - } - - # Analysis configuration - AnalysisConfig = @{ - Enabled = -not $SkipAnalysis - SettingsPath = './PSScriptAnalyzerSettings.psd1' - Severity = @('Error', 'Warning', 'Information') - ExcludeRules = @() - } - - # Publish configuration - PublishConfig = @{ - Repository = 'PSGallery' - ApiKey = $NuGetApiKey - Tags = @('Plaster', 'CodeGenerator', 'Scaffold', 'Template', 'PowerShell7') - } -} - -# Bootstrap required modules -task Bootstrap { - Write-Host "Bootstrapping build dependencies..." -ForegroundColor Cyan - - $requiredModules = @( - @{ Name = 'Pester'; MinimumVersion = '5.0.0' } - @{ Name = 'PSScriptAnalyzer'; MinimumVersion = '1.19.0' } - @{ Name = 'platyPS'; MinimumVersion = '0.14.0' } - @{ Name = 'PowerShellGet'; MinimumVersion = '2.2.0' } - ) - - foreach ($module in $requiredModules) { - $installed = Get-Module -Name $module.Name -ListAvailable | - Where-Object { $_.Version -ge $module.MinimumVersion } | - Sort-Object Version -Descending | - Select-Object -First 1 - - if (-not $installed) { - Write-Host "Installing $($module.Name) >= $($module.MinimumVersion)..." -ForegroundColor Yellow - Install-Module -Name $module.Name -MinimumVersion $module.MinimumVersion -Scope CurrentUser -Force -AllowClobber - } else { - Write-Host "$($module.Name) $($installed.Version) is already installed" -ForegroundColor Green - } - - # Cache tool paths - $script:BuildConfig.Tools[$module.Name] = Get-Module -Name $module.Name -ListAvailable | - Sort-Object Version -Descending | - Select-Object -First 1 - } -} - -# Clean build artifacts -task Clean { - Write-Host "Cleaning build artifacts..." -ForegroundColor Cyan - - if (Test-Path $script:BuildConfig.OutputPath) { - Remove-Item -Path $script:BuildConfig.OutputPath -Recurse -Force - Write-Host "Removed output directory: $($script:BuildConfig.OutputPath)" -ForegroundColor Green - } - - # Clean any temp files - Get-ChildItem -Path . -Filter "*.tmp" -Recurse | Remove-Item -Force - Get-ChildItem -Path . -Filter "TestResults*.xml" -Recurse | Remove-Item -Force -} - -# Initialize build environment -task Init Clean, { - Write-Host "Initializing build environment..." -ForegroundColor Cyan - - # Create output directory - if (-not (Test-Path $script:BuildConfig.OutputPath)) { - New-Item -Path $script:BuildConfig.OutputPath -ItemType Directory -Force | Out-Null - Write-Host "Created output directory: $($script:BuildConfig.OutputPath)" -ForegroundColor Green - } - - # Read module version from manifest - $manifestPath = Join-Path $script:BuildConfig.SourcePath "$($script:BuildConfig.ModuleName).psd1" - if (Test-Path $manifestPath) { - $manifest = Test-ModuleManifest -Path $manifestPath - $script:BuildConfig.ModuleVersion = $manifest.Version - Write-Host "Module version: $($script:BuildConfig.ModuleVersion)" -ForegroundColor Green - } else { - throw "Module manifest not found at: $manifestPath" - } -} - -# Build the module -task Build Init, { - Write-Host "Building module..." -ForegroundColor Cyan - - $moduleOutputPath = Join-Path $script:BuildConfig.OutputPath $script:BuildConfig.ModuleName - - # Create module directory - if (-not (Test-Path $moduleOutputPath)) { - New-Item -Path $moduleOutputPath -ItemType Directory -Force | Out-Null - } - - # Copy source files - $sourceFiles = @( - '*.psd1', '*.psm1', '*.ps1', '*.dll' - 'Templates', 'Schema', 'en-US' - ) - - foreach ($pattern in $sourceFiles) { - $items = Get-ChildItem -Path $script:BuildConfig.SourcePath -Filter $pattern -ErrorAction SilentlyContinue - if ($items) { - foreach ($item in $items) { - $destination = Join-Path $moduleOutputPath $item.Name - if ($item.PSIsContainer) { - Copy-Item -Path $item.FullName -Destination $destination -Recurse -Force - } else { - Copy-Item -Path $item.FullName -Destination $destination -Force + # Build task(s) to execute + [parameter(ParameterSetName = 'task', position = 0)] + [ArgumentCompleter( { + param($Command, $Parameter, $WordToComplete, $CommandAst, $FakeBoundParams) + $psakeFile = './psakeFile.ps1' + switch ($Parameter) { + 'Task' { + if ([string]::IsNullOrEmpty($WordToComplete)) { + Get-PSakeScriptTasks -BuildFile $psakeFile | Select-Object -ExpandProperty Name + } else { + Get-PSakeScriptTasks -BuildFile $psakeFile | + Where-Object { $_.Name -match $WordToComplete } | + Select-Object -ExpandProperty Name + } + } + default { } - Write-Verbose "Copied: $($item.Name)" } - } - } - - # Update module manifest with build metadata - $manifestPath = Join-Path $moduleOutputPath "$($script:BuildConfig.ModuleName).psd1" - if (Test-Path $manifestPath) { - $content = Get-Content -Path $manifestPath -Raw - - # Add build metadata to private data - $buildInfo = @{ - BuildDate = Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ' - BuildNumber = $script:BuildConfig.BuildNumber - PSVersion = $PSVersionTable.PSVersion - Platform = [System.Runtime.InteropServices.RuntimeInformation]::OSDescription - } - - $buildInfoJson = $buildInfo | ConvertTo-Json -Compress - $content = $content -replace '(# Build metadata placeholder)', "BuildInfo = '$buildInfoJson'" - - Set-Content -Path $manifestPath -Value $content -Encoding UTF8 - Write-Host "Updated manifest with build metadata" -ForegroundColor Green - } - - Write-Host "Module built successfully at: $moduleOutputPath" -ForegroundColor Green -} - -# Run PSScriptAnalyzer -task Analyze { - if (-not $script:BuildConfig.AnalysisConfig.Enabled) { - Write-Host "Analysis skipped (disabled)" -ForegroundColor Yellow - return - } - - Write-Host "Running PSScriptAnalyzer..." -ForegroundColor Cyan - - Import-Module PSScriptAnalyzer -Force - - $analyzeParams = @{ - Path = $script:BuildConfig.SourcePath - Recurse = $true - Settings = $script:BuildConfig.AnalysisConfig.SettingsPath - Severity = $script:BuildConfig.AnalysisConfig.Severity - } + })] + [string[]]$Task = 'default', - if ($script:BuildConfig.AnalysisConfig.ExcludeRules) { - $analyzeParams.ExcludeRule = $script:BuildConfig.AnalysisConfig.ExcludeRules - } - - $results = Invoke-ScriptAnalyzer @analyzeParams - - if ($results) { - $results | Format-Table -AutoSize - - $errors = $results | Where-Object Severity -eq 'Error' - $warnings = $results | Where-Object Severity -eq 'Warning' - - Write-Host "Analysis completed: $($errors.Count) errors, $($warnings.Count) warnings" -ForegroundColor Yellow - - if ($errors) { - throw "PSScriptAnalyzer found $($errors.Count) error(s). Build cannot continue." - } - } else { - Write-Host "PSScriptAnalyzer found no issues" -ForegroundColor Green - } -} - -# Run Pester tests -task Test Build, { - if ($SkipTests) { - Write-Host "Tests skipped" -ForegroundColor Yellow - return - } - - Write-Host "Running Pester tests..." -ForegroundColor Cyan - - Import-Module Pester -Force -MinimumVersion 5.0 - - # Configure Pester - $pesterConfig = New-PesterConfiguration - - # Run settings - $pesterConfig.Run.Path = $script:BuildConfig.TestPath - $pesterConfig.Run.PassThru = $true - - # Output settings - $pesterConfig.Output.Verbosity = 'Detailed' - - # Test result settings - $pesterConfig.TestResult.Enabled = $true - $pesterConfig.TestResult.OutputFormat = $script:BuildConfig.TestConfig.OutputFormat - $pesterConfig.TestResult.OutputPath = $script:BuildConfig.TestConfig.OutputPath - - # Code coverage settings - if ($script:BuildConfig.TestConfig.CodeCoverage.Enabled) { - $pesterConfig.CodeCoverage.Enabled = $true - $pesterConfig.CodeCoverage.OutputFormat = $script:BuildConfig.TestConfig.CodeCoverage.OutputFormat - $pesterConfig.CodeCoverage.OutputPath = $script:BuildConfig.TestConfig.CodeCoverage.OutputPath + # Bootstrap dependencies + [switch]$Bootstrap, - # Include source files for coverage - $sourceFiles = Get-ChildItem -Path $script:BuildConfig.SourcePath -Filter "*.ps1" -Recurse | - Where-Object { $_.Name -notmatch '\.Tests\.ps1' } | - ForEach-Object { $_.FullName } + # List available build tasks + [parameter(ParameterSetName = 'Help')] + [switch]$Help, - if ($sourceFiles) { - $pesterConfig.CodeCoverage.Path = $sourceFiles - } - } - - # Run tests - $testResults = Invoke-Pester -Configuration $pesterConfig - - # Check results - if ($testResults.FailedCount -gt 0) { - throw "Pester tests failed: $($testResults.FailedCount) failed, $($testResults.PassedCount) passed" - } - - # Check code coverage - if ($script:BuildConfig.TestConfig.CodeCoverage.Enabled -and $testResults.CodeCoverage) { - $coveragePercent = [math]::Round($testResults.CodeCoverage.CoveragePercent, 2) - $threshold = $script:BuildConfig.TestConfig.CodeCoverage.Threshold - - Write-Host "Code coverage: $coveragePercent%" -ForegroundColor $(if ($coveragePercent -ge $threshold) { 'Green' } else { 'Red' }) - - if ($coveragePercent -lt $threshold) { - Write-Warning "Code coverage ($coveragePercent%) is below threshold ($threshold%)" - } - } + # Optional properties to pass to psake + [hashtable]$Properties, - Write-Host "All tests passed: $($testResults.PassedCount) passed, $($testResults.FailedCount) failed" -ForegroundColor Green -} - -# Generate documentation -task Docs Build, { - Write-Host "Generating documentation..." -ForegroundColor Cyan - - try { - Import-Module platyPS -Force - - $moduleOutputPath = Join-Path $script:BuildConfig.OutputPath $script:BuildConfig.ModuleName - $docsOutputPath = Join-Path $script:BuildConfig.OutputPath 'docs' - - # Import the built module - Import-Module $moduleOutputPath -Force - - # Create docs directory - if (-not (Test-Path $docsOutputPath)) { - New-Item -Path $docsOutputPath -ItemType Directory -Force | Out-Null - } + # Optional parameters to pass to psake + [hashtable]$Parameters +) - # Generate markdown help - New-MarkdownHelp -Module $script:BuildConfig.ModuleName -OutputFolder $docsOutputPath -Force +$ErrorActionPreference = 'Stop' - # Generate external help - $helpOutputPath = Join-Path $moduleOutputPath 'en-US' - if (-not (Test-Path $helpOutputPath)) { - New-Item -Path $helpOutputPath -ItemType Directory -Force | Out-Null +# Bootstrap dependencies +if ($Bootstrap.IsPresent) { + PackageManagement\Get-PackageProvider -Name Nuget -ForceBootstrap | Out-Null + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + if ((Test-Path -Path ./requirements.psd1)) { + if (-not (Get-Module -Name PSDepend -ListAvailable)) { + Install-Module -Name PSDepend -Repository PSGallery -Scope CurrentUser -Force } - - New-ExternalHelp -Path $docsOutputPath -OutputPath $helpOutputPath -Force - - Write-Host "Documentation generated successfully" -ForegroundColor Green - } catch { - Write-Warning "Documentation generation failed: $($_.Exception.Message)" - } finally { - Remove-Module $script:BuildConfig.ModuleName -ErrorAction SilentlyContinue - } -} - -# Package the module -task Package Build, Test, Analyze, Docs, { - Write-Host "Packaging module..." -ForegroundColor Cyan - - $moduleOutputPath = Join-Path $script:BuildConfig.OutputPath $script:BuildConfig.ModuleName - $packagePath = Join-Path $script:BuildConfig.OutputPath "$($script:BuildConfig.ModuleName).$($script:BuildConfig.ModuleVersion).nupkg" - - # Create a staging directory for packaging - $stagingPath = Join-Path $script:BuildConfig.OutputPath 'staging' - if (Test-Path $stagingPath) { - Remove-Item -Path $stagingPath -Recurse -Force - } - - # Copy module to staging - Copy-Item -Path $moduleOutputPath -Destination $stagingPath -Recurse -Force - - # Create package manifest - $packageManifest = @{ - ModuleName = $script:BuildConfig.ModuleName - ModuleVersion = $script:BuildConfig.ModuleVersion - BuildDate = Get-Date - Configuration = $script:BuildConfig.Configuration - Platform = $PSVersionTable.Platform ?? 'Windows' - } - - $packageManifest | ConvertTo-Json | Out-File -FilePath (Join-Path $stagingPath 'package.json') -Encoding UTF8 - - Write-Host "Module packaged at: $stagingPath" -ForegroundColor Green -} - -# Install the module locally -task Install Package, { - Write-Host "Installing module locally..." -ForegroundColor Cyan - - $moduleOutputPath = Join-Path $script:BuildConfig.OutputPath $script:BuildConfig.ModuleName - - # Determine installation path - $installPath = if ($IsWindows) { - Join-Path $env:USERPROFILE "Documents\PowerShell\Modules\$($script:BuildConfig.ModuleName)" + Import-Module -Name PSDepend -Verbose:$false + Invoke-PSDepend -Path .\requirements.psd1 -Install -Import -Force -WarningAction SilentlyContinue } else { - Join-Path $HOME ".local/share/powershell/Modules/$($script:BuildConfig.ModuleName)" - } - - # Remove existing installation - if (Test-Path $installPath) { - Remove-Item -Path $installPath -Recurse -Force - Write-Host "Removed existing installation: $installPath" -ForegroundColor Yellow - } - - # Create installation directory - $versionPath = Join-Path $installPath $script:BuildConfig.ModuleVersion - New-Item -Path $versionPath -ItemType Directory -Force | Out-Null - - # Copy module files - Copy-Item -Path "$moduleOutputPath\*" -Destination $versionPath -Recurse -Force - - Write-Host "Module installed at: $versionPath" -ForegroundColor Green - - # Test installation - try { - Import-Module $script:BuildConfig.ModuleName -Force - $importedModule = Get-Module $script:BuildConfig.ModuleName - Write-Host "Installation verified: $($importedModule.Name) v$($importedModule.Version)" -ForegroundColor Green - } catch { - throw "Installation verification failed: $($_.Exception.Message)" - } finally { - Remove-Module $script:BuildConfig.ModuleName -ErrorAction SilentlyContinue + Write-Warning 'No [requirements.psd1] found. Skipping build dependency installation.' } } -# Publish to PowerShell Gallery -task Publish Package, { - if (-not $PublishToGallery) { - Write-Host "Publish skipped (not requested)" -ForegroundColor Yellow - return - } - - if (-not $script:BuildConfig.PublishConfig.ApiKey) { - throw "NuGetApiKey is required for publishing to PowerShell Gallery" - } - - Write-Host "Publishing to PowerShell Gallery..." -ForegroundColor Cyan - - $moduleOutputPath = Join-Path $script:BuildConfig.OutputPath $script:BuildConfig.ModuleName - - $publishParams = @{ - Path = $moduleOutputPath - Repository = $script:BuildConfig.PublishConfig.Repository - NuGetApiKey = $script:BuildConfig.PublishConfig.ApiKey - Force = $true - Verbose = $true - } - - try { - Publish-Module @publishParams - Write-Host "Module published successfully to $($script:BuildConfig.PublishConfig.Repository)" -ForegroundColor Green - } catch { - throw "Publishing failed: $($_.Exception.Message)" - } -} - -# Default task -task . Bootstrap, Build - -# CI/CD task -task CI Bootstrap, Clean, Build, Analyze, Test, Package - -# Full pipeline task -task Pipeline Bootstrap, Clean, Build, Analyze, Test, Docs, Package, Install - -# Release task -task Release Bootstrap, Clean, Build, Analyze, Test, Docs, Package, Publish - -# Show build configuration -task ShowConfig { - Write-Host "Build Configuration:" -ForegroundColor Cyan - Write-Host " Module Name: $($script:BuildConfig.ModuleName)" -ForegroundColor White - Write-Host " Module Version: $($script:BuildConfig.ModuleVersion)" -ForegroundColor White - Write-Host " Configuration: $($script:BuildConfig.Configuration)" -ForegroundColor White - Write-Host " Output Path: $($script:BuildConfig.OutputPath)" -ForegroundColor White - Write-Host " Source Path: $($script:BuildConfig.SourcePath)" -ForegroundColor White - Write-Host " Test Path: $($script:BuildConfig.TestPath)" -ForegroundColor White - Write-Host " Platform: $($PSVersionTable.Platform ?? 'Windows')" -ForegroundColor White - Write-Host " PowerShell Version: $($PSVersionTable.PSVersion)" -ForegroundColor White - Write-Host " Is CI: $($script:BuildConfig.IsCI)" -ForegroundColor White +# Execute psake task(s) +$psakeFile = './psakeFile.ps1' +if ($PSCmdlet.ParameterSetName -eq 'Help') { + Get-PSakeScriptTasks -BuildFile $psakeFile | + Format-Table -Property Name, Description, Alias, DependsOn +} else { + Set-BuildEnvironment -Force + Invoke-psake -buildFile $psakeFile -taskList $Task -nologo -properties $Properties -parameters $Parameters + exit ([int](-not $psake.build_success)) } \ No newline at end of file diff --git a/build.psake.ps1 b/build.psake.ps1 deleted file mode 100644 index 513ded1..0000000 --- a/build.psake.ps1 +++ /dev/null @@ -1,693 +0,0 @@ -#Requires -Modules psake - -############################################################################## -# DO NOT MODIFY THIS FILE! Modify build.settings.ps1 instead. -############################################################################## - -############################################################################## -# This is the PowerShell Module psake build script. It defines the following tasks: -# -# Clean, Build, Sign, BuildHelp, Install, Test and Publish. -# -# The default task is Build. This task copies the appropriate files from the -# $SrcRootDir under the $OutDir. Later, other tasks such as Sign and BuildHelp -# will further modify the contents of $OutDir and add new files. -# -# The Sign task will only sign scripts if the $SignScripts variable is set to -# $true. A code-signing certificate is required for this task to complete. -# -# The BuildHelp task invokes platyPS to generate markdown files from -# comment-based help for your exported commands. platyPS then generates -# a help file for your module from the markdown files. -# -# The Install task simplies copies the module folder under $OutDir to your -# profile's Modules folder. -# -# The Test task invokes Pester on the $TestRootDir. -# -# The Publish task uses the Publish-Module command to publish -# to either the PowerShell Gallery (the default) or you can change -# the $PublishRepository property to the name of an alternate repository. -# Note: the Publish task requires that the Test task execute without failures. -# -# You can exeute a specific task, such as the Test task by running the -# following command: -# -# PS C:\> invoke-psake build.psake.ps1 -taskList Test -# -# You can execute the Publish task with the following command. -# The first time you execute the Publish task, you will be prompted to enter -# your PowerShell Gallery NuGetApiKey. After entering the key, it is encrypted -# and stored so you will not have to enter it again. -# -# PS C:\> invoke-psake build.psake.ps1 -taskList Publish -# -# You can verify the stored and encrypted NuGetApiKey by running the following -# command which will display a portion of your NuGetApiKey in plain text. -# -# PS C:\> invoke-psake build.psake.ps1 -taskList ShowApiKey -# -# You can store a new NuGetApiKey with this command. You can leave off -# the -properties parameter and you'll be prompted for the key. -# -# PS C:\> invoke-psake build.psake.ps1 -taskList StoreApiKey -properties @{NuGetApiKey='test123'} -# - -############################################################################### -# Dot source the user's customized properties and extension tasks. -############################################################################### -. $PSScriptRoot\build.settings.ps1 - -############################################################################### -# Private properties. -############################################################################### -Properties { - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] - $ModuleOutDir = "$OutDir\$ModuleName" - - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] - $UpdatableHelpOutDir = "$OutDir\UpdatableHelp" - - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] - $SharedProperties = @{} - - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssigments', '')] - $LineSep = "-" * 78 -} - -############################################################################### -# Core task implementations. Avoid modifying these tasks. -############################################################################### -Task default -depends Build - -Task Init -requiredVariables OutDir { - if (!(Test-Path -LiteralPath $OutDir)) { - New-Item $OutDir -ItemType Directory -Verbose:$VerbosePreference > $null - } - else { - Write-Verbose "$($psake.context.currentTaskName) - directory already exists '$OutDir'." - } -} - -Task Clean -depends Init -requiredVariables OutDir { - # Maybe a bit paranoid but this task nuked \ on my laptop. Good thing I was not running as admin. - if ($OutDir.Length -gt 3) { - Get-ChildItem $OutDir | Remove-Item -Recurse -Force -Verbose:$VerbosePreference - } - else { - Write-Verbose "$($psake.context.currentTaskName) - `$OutDir '$OutDir' must be longer than 3 characters." - } -} - -Task StageFiles -depends Init, Clean, BeforeStageFiles, CoreStageFiles, AfterStageFiles { -} - -Task CoreStageFiles -requiredVariables ModuleOutDir, SrcRootDir { - if (!(Test-Path -LiteralPath $ModuleOutDir)) { - New-Item $ModuleOutDir -ItemType Directory -Verbose:$VerbosePreference > $null - } - else { - Write-Verbose "$($psake.context.currentTaskName) - directory already exists '$ModuleOutDir'." - } - - Copy-Item -Path $SrcRootDir\* -Destination $ModuleOutDir -Recurse -Exclude $Exclude -Verbose:$VerbosePreference -} - -Task Build -depends Init, Clean, BeforeBuild, StageFiles, Analyze, Sign, AfterBuild { -} - -Task Analyze -depends StageFiles ` - -requiredVariables ModuleOutDir, ScriptAnalysisEnabled, ScriptAnalysisFailBuildOnSeverityLevel, ScriptAnalyzerSettingsPath { - if (!$ScriptAnalysisEnabled) { - "Script analysis is not enabled. Skipping $($psake.context.currentTaskName) task." - return - } - - if (!(Get-Module PSScriptAnalyzer -ListAvailable)) { - "PSScriptAnalyzer module is not installed. Skipping $($psake.context.currentTaskName) task." - return - } - - "ScriptAnalysisFailBuildOnSeverityLevel set to: $ScriptAnalysisFailBuildOnSeverityLevel" - - $analysisResult = Invoke-ScriptAnalyzer -Path $ModuleOutDir -Settings $ScriptAnalyzerSettingsPath -Recurse -Verbose:$VerbosePreference - $analysisResult | Format-Table - switch ($ScriptAnalysisFailBuildOnSeverityLevel) { - 'None' { - return - } - 'Error' { - Assert -conditionToCheck ( - ($analysisResult | Where-Object Severity -eq 'Error').Count -eq 0 - ) -failureMessage 'One or more ScriptAnalyzer errors were found. Build cannot continue!' - } - 'Warning' { - Assert -conditionToCheck ( - ($analysisResult | Where-Object { - $_.Severity -eq 'Warning' -or $_.Severity -eq 'Error' - }).Count -eq 0) -failureMessage 'One or more ScriptAnalyzer warnings were found. Build cannot continue!' - } - default { - Assert -conditionToCheck ( - $analysisResult.Count -eq 0 - ) -failureMessage 'One or more ScriptAnalyzer issues were found. Build cannot continue!' - } - } -} - -Task Sign -depends StageFiles -requiredVariables CertPath, SettingsPath, ScriptSigningEnabled { - if (!$ScriptSigningEnabled) { - "Script signing is not enabled. Skipping $($psake.context.currentTaskName) task." - return - } - - $validCodeSigningCerts = Get-ChildItem -Path $CertPath -CodeSigningCert -Recurse | Where-Object NotAfter -ge (Get-Date) - if (!$validCodeSigningCerts) { - throw "There are no non-expired code-signing certificates in $CertPath. You can either install " + - "a code-signing certificate into the certificate store or disable script analysis in build.settings.ps1." - } - - $certSubjectNameKey = "CertSubjectName" - $storeCertSubjectName = $true - - # Get the subject name of the code-signing certificate to be used for script signing. - if (!$CertSubjectName -and ($CertSubjectName = GetSetting -Key $certSubjectNameKey -Path $SettingsPath)) { - $storeCertSubjectName = $false - } - elseif (!$CertSubjectName) { - "A code-signing certificate has not been specified." - "The following non-expired, code-signing certificates are available in your certificate store:" - $validCodeSigningCerts | Format-List Subject,Issuer,Thumbprint,NotBefore,NotAfter - - $CertSubjectName = Read-Host -Prompt 'Enter the subject name (case-sensitive) of the certificate to use for script signing' - } - - # Find a code-signing certificate that matches the specified subject name. - $certificate = $validCodeSigningCerts | - Where-Object { $_.SubjectName.Name -cmatch [regex]::Escape($CertSubjectName) } | - Sort-Object NotAfter -Descending | Select-Object -First 1 - - if ($certificate) { - $SharedProperties.CodeSigningCertificate = $certificate - - if ($storeCertSubjectName) { - SetSetting -Key $certSubjectNameKey -Value $certificate.SubjectName.Name -Path $SettingsPath - "The new certificate subject name has been stored in ${SettingsPath}." - } - else { - "Using stored certificate subject name $CertSubjectName from ${SettingsPath}." - } - - $LineSep - "Using code-signing certificate: $certificate" - $LineSep - - $files = @(Get-ChildItem -Path $ModuleOutDir\* -Recurse -Include *.ps1,*.psm1) - foreach ($file in $files) { - $setAuthSigParams = @{ - FilePath = $file.FullName - Certificate = $certificate - Verbose = $VerbosePreference - } - - $result = Microsoft.PowerShell.Security\Set-AuthenticodeSignature @setAuthSigParams - if ($result.Status -ne 'Valid') { - throw "Failed to sign script: $($file.FullName)." - } - - "Successfully signed script: $($file.Name)" - } - } - else { - $expiredCert = Get-ChildItem -Path $CertPath -CodeSigningCert -Recurse | - Where-Object { ($_.SubjectName.Name -cmatch [regex]::Escape($CertSubjectName)) -and - ($_.NotAfter -lt (Get-Date)) } - Sort-Object NotAfter -Descending | Select-Object -First 1 - - if ($expiredCert) { - throw "The code-signing certificate `"$($expiredCert.SubjectName.Name)`" EXPIRED on $($expiredCert.NotAfter)." - } - - throw 'No valid certificate subject name supplied or stored.' - } -} - -Task BuildHelp -depends Build, BeforeBuildHelp, GenerateMarkdown, GenerateHelpFiles, AfterBuildHelp { -} - -Task GenerateMarkdown -requiredVariables DefaultLocale, DocsRootDir, ModuleName, ModuleOutDir { - if (!(Get-Module platyPS -ListAvailable)) { - "platyPS module is not installed. Skipping $($psake.context.currentTaskName) task." - return - } - - $moduleInfo = Import-Module $ModuleOutDir\$ModuleName.psd1 -Global -Force -PassThru - - try { - if ($moduleInfo.ExportedCommands.Count -eq 0) { - "No commands have been exported. Skipping $($psake.context.currentTaskName) task." - return - } - - if (!(Test-Path -LiteralPath $DocsRootDir)) { - New-Item $DocsRootDir -ItemType Directory > $null - } - - if (Get-ChildItem -LiteralPath $DocsRootDir -Filter *.md -Recurse) { - Get-ChildItem -LiteralPath $DocsRootDir -Directory | ForEach-Object { - Update-MarkdownHelp -Path $_.FullName -Verbose:$VerbosePreference > $null - } - } - - # ErrorAction set to SilentlyContinue so this command will not overwrite an existing MD file. - New-MarkdownHelp -Module $ModuleName -Locale $DefaultLocale -OutputFolder $DocsRootDir\$DefaultLocale ` - -WithModulePage -ErrorAction SilentlyContinue -Verbose:$VerbosePreference > $null - } - finally { - Remove-Module $ModuleName - } -} - -Task GenerateHelpFiles -requiredVariables DocsRootDir, ModuleName, ModuleOutDir, OutDir { - if (!(Get-Module platyPS -ListAvailable)) { - "platyPS module is not installed. Skipping $($psake.context.currentTaskName) task." - return - } - - if (!(Get-ChildItem -LiteralPath $DocsRootDir -Filter *.md -Recurse -ErrorAction SilentlyContinue)) { - "No markdown help files to process. Skipping $($psake.context.currentTaskName) task." - return - } - - $helpLocales = (Get-ChildItem -Path $DocsRootDir -Directory).Name - - # Generate the module's primary MAML help file. - foreach ($locale in $helpLocales) { - New-ExternalHelp -Path $DocsRootDir\$locale -OutputPath $ModuleOutDir\$locale -Force ` - -ErrorAction SilentlyContinue -Verbose:$VerbosePreference > $null - } -} - -Task BuildUpdatableHelp -depends BuildHelp, BeforeBuildUpdatableHelp, CoreBuildUpdatableHelp, AfterBuildUpdatableHelp { -} - -Task CoreBuildUpdatableHelp -requiredVariables DocsRootDir, ModuleName, UpdatableHelpOutDir { - if (!(Get-Module platyPS -ListAvailable)) { - "platyPS module is not installed. Skipping $($psake.context.currentTaskName) task." - return - } - - $helpLocales = (Get-ChildItem -Path $DocsRootDir -Directory).Name - - # Create updatable help output directory. - if (!(Test-Path -LiteralPath $UpdatableHelpOutDir)) { - New-Item $UpdatableHelpOutDir -ItemType Directory -Verbose:$VerbosePreference > $null - } - else { - Write-Verbose "$($psake.context.currentTaskName) - directory already exists '$UpdatableHelpOutDir'." - Get-ChildItem $UpdatableHelpOutDir | Remove-Item -Recurse -Force -Verbose:$VerbosePreference - } - - # Generate updatable help files. Note: this will currently update the version number in the module's MD - # file in the metadata. - foreach ($locale in $helpLocales) { - New-ExternalHelpCab -CabFilesFolder $ModuleOutDir\$locale -LandingPagePath $DocsRootDir\$locale\$ModuleName.md ` - -OutputFolder $UpdatableHelpOutDir -Verbose:$VerbosePreference > $null - } -} - -Task GenerateFileCatalog -depends Build, BuildHelp, BeforeGenerateFileCatalog, CoreGenerateFileCatalog, AfterGenerateFileCatalog { -} - -Task CoreGenerateFileCatalog -requiredVariables CatalogGenerationEnabled, CatalogVersion, ModuleName, ModuleOutDir, OutDir { - if (!$CatalogGenerationEnabled) { - "FileCatalog generation is not enabled. Skipping $($psake.context.currentTaskName) task." - return - } - - if (!(Get-Command Microsoft.PowerShell.Security\New-FileCatalog -ErrorAction SilentlyContinue)) { - "FileCatalog commands not available on this version of PowerShell. Skipping $($psake.context.currentTaskName) task." - return - } - - $catalogFilePath = "$OutDir\$ModuleName.cat" - - $newFileCatalogParams = @{ - Path = $ModuleOutDir - CatalogFilePath = $catalogFilePath - CatalogVersion = $CatalogVersion - Verbose = $VerbosePreference - } - - Microsoft.PowerShell.Security\New-FileCatalog @newFileCatalogParams > $null - - if ($ScriptSigningEnabled) { - if ($SharedProperties.CodeSigningCertificate) { - $setAuthSigParams = @{ - FilePath = $catalogFilePath - Certificate = $SharedProperties.CodeSigningCertificate - Verbose = $VerbosePreference - } - - $result = Microsoft.PowerShell.Security\Set-AuthenticodeSignature @setAuthSigParams - if ($result.Status -ne 'Valid') { - throw "Failed to sign file catalog: $($catalogFilePath)." - } - - "Successfully signed file catalog: $($catalogFilePath)" - } - else { - "No code-signing certificate was found to sign the file catalog." - } - } - else { - "Script signing is not enabled. Skipping signing of file catalog." - } - - Move-Item -LiteralPath $newFileCatalogParams.CatalogFilePath -Destination $ModuleOutDir -} - -Task Install -depends Build, BuildHelp, GenerateFileCatalog, BeforeInstall, CoreInstall, AfterInstall { -} - -Task CoreInstall -requiredVariables ModuleOutDir { - if (!(Test-Path -LiteralPath $InstallPath)) { - Write-Verbose 'Creating install directory' - New-Item -Path $InstallPath -ItemType Directory -Verbose:$VerbosePreference > $null - } - - Copy-Item -Path $ModuleOutDir\* -Destination $InstallPath -Verbose:$VerbosePreference -Recurse -Force - "Module installed into $InstallPath" -} - -Task Test -depends Build -requiredVariables TestRootDir, ModuleName, CodeCoverageEnabled, CodeCoverageFiles { - if (!(Get-Module Pester -ListAvailable)) { - "Pester module is not installed. Skipping $($psake.context.currentTaskName) task." - return - } - - Import-Module Pester - - try { - Microsoft.PowerShell.Management\Push-Location -LiteralPath $TestRootDir - - if ($TestOutputFile) { - $testing = @{ - OutputFile = $TestOutputFile - OutputFormat = $TestOutputFormat - PassThru = $true - Verbose = $VerbosePreference - } - } - else { - $testing = @{ - PassThru = $true - Verbose = $VerbosePreference - } - } - - # To control the Pester code coverage, a boolean $CodeCoverageEnabled is used. - if ($CodeCoverageEnabled) { - $testing.CodeCoverage = $CodeCoverageFiles - } - - $testResult = Invoke-Pester @testing - - Assert -conditionToCheck ( - $testResult.FailedCount -eq 0 - ) -failureMessage "One or more Pester tests failed, build cannot continue." - - if ($CodeCoverageEnabled) { - $testCoverage = [int]($testResult.CodeCoverage.NumberOfCommandsExecuted / - $testResult.CodeCoverage.NumberOfCommandsAnalyzed * 100) - "Pester code coverage on specified files: ${testCoverage}%" - } - } - finally { - Microsoft.PowerShell.Management\Pop-Location - Remove-Module $ModuleName -ErrorAction SilentlyContinue - } -} - -Task Publish -depends Build, Test, BuildHelp, GenerateFileCatalog, BeforePublish, CorePublish, AfterPublish { -} - -Task CorePublish -requiredVariables SettingsPath, ModuleOutDir { - $publishParams = @{ - Path = $ModuleOutDir - NuGetApiKey = $NuGetApiKey - } - - # Publishing to the PSGallery requires an API key, so get it. - if ($NuGetApiKey) { - "Using script embedded NuGetApiKey" - } - elseif ($NuGetApiKey = GetSetting -Path $SettingsPath -Key NuGetApiKey) { - "Using stored NuGetApiKey" - } - else { - $promptForKeyCredParams = @{ - DestinationPath = $SettingsPath - Message = 'Enter your NuGet API key in the password field' - Key = 'NuGetApiKey' - } - - $cred = PromptUserForCredentialAndStorePassword @promptForKeyCredParams - $NuGetApiKey = $cred.GetNetworkCredential().Password - "The NuGetApiKey has been stored in $SettingsPath" - } - - $publishParams = @{ - Path = $ModuleOutDir - NuGetApiKey = $NuGetApiKey - } - - # If an alternate repository is specified, set the appropriate parameter. - if ($PublishRepository) { - $publishParams['Repository'] = $PublishRepository - } - - # Consider not using -ReleaseNotes parameter when Update-ModuleManifest has been fixed. - if ($ReleaseNotesPath) { - $publishParams['ReleaseNotes'] = @(Get-Content $ReleaseNotesPath) - } - - "Calling Publish-Module..." - Publish-Module @publishParams -} - -############################################################################### -# Secondary/utility tasks - typically used to manage stored build settings. -############################################################################### - -Task ? -description 'Lists the available tasks' { - "Available tasks:" - $psake.context.Peek().Tasks.Keys | Sort-Object -} - -Task RemoveApiKey -requiredVariables SettingsPath { - if (GetSetting -Path $SettingsPath -Key NuGetApiKey) { - RemoveSetting -Path $SettingsPath -Key NuGetApiKey - } -} - -Task StoreApiKey -requiredVariables SettingsPath { - $promptForKeyCredParams = @{ - DestinationPath = $SettingsPath - Message = 'Enter your NuGet API key in the password field' - Key = 'NuGetApiKey' - } - - PromptUserForCredentialAndStorePassword @promptForKeyCredParams - "The NuGetApiKey has been stored in $SettingsPath" -} - -Task ShowApiKey -requiredVariables SettingsPath { - $OFS = "" - if ($NuGetApiKey) { - "The embedded (partial) NuGetApiKey is: $($NuGetApiKey[0..7])" - } - elseif ($NuGetApiKey = GetSetting -Path $SettingsPath -Key NuGetApiKey) { - "The stored (partial) NuGetApiKey is: $($NuGetApiKey[0..7])" - } - else { - "The NuGetApiKey has not been provided or stored." - return - } - - "To see the full key, use the task 'ShowFullApiKey'" -} - -Task ShowFullApiKey -requiredVariables SettingsPath { - if ($NuGetApiKey) { - "The embedded NuGetApiKey is: $NuGetApiKey" - } - elseif ($NuGetApiKey = GetSetting -Path $SettingsPath -Key NuGetApiKey) { - "The stored NuGetApiKey is: $NuGetApiKey" - } - else { - "The NuGetApiKey has not been provided or stored." - } -} - -Task RemoveCertSubjectName -requiredVariables SettingsPath { - if (GetSetting -Path $SettingsPath -Key CertSubjectName) { - RemoveSetting -Path $SettingsPath -Key CertSubjectName - } -} - -Task StoreCertSubjectName -requiredVariables SettingsPath { - $certSubjectName = 'CN=' - $certSubjectName += Read-Host -Prompt 'Enter the certificate subject name for script signing. Use exact casing, CN= prefix will be added' - SetSetting -Key CertSubjectName -Value $certSubjectName -Path $SettingsPath - "The new certificate subject name '$certSubjectName' has been stored in ${SettingsPath}." -} - -Task ShowCertSubjectName -requiredVariables SettingsPath { - $CertSubjectName = GetSetting -Path $SettingsPath -Key CertSubjectName - "The stored certificate is: $CertSubjectName" - - $cert = Get-ChildItem -Path Cert:\CurrentUser\My -CodeSigningCert | - Where-Object { $_.Subject -eq $CertSubjectName -and $_.NotAfter -gt (Get-Date) } | - Sort-Object -Property NotAfter -Descending | Select-Object -First 1 - - if ($cert) { - "A valid certificate for the subject $CertSubjectName has been found" - } - else { - 'A valid certificate has not been found' - } -} - -############################################################################### -# Helper functions -############################################################################### - -function PromptUserForCredentialAndStorePassword { - [Diagnostics.CodeAnalysis.SuppressMessage("PSProvideDefaultParameterValue", '')] - param( - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $DestinationPath, - - [Parameter(Mandatory)] - [string] - $Message, - - [Parameter(Mandatory, ParameterSetName = 'SaveSetting')] - [string] - $Key - ) - - $cred = Get-Credential -Message $Message -UserName "ignored" - if ($DestinationPath) { - SetSetting -Key $Key -Value $cred.Password -Path $DestinationPath - } - - $cred -} - -function AddSetting { - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope='Function')] - param( - [Parameter(Mandatory)] - [string]$Key, - - [Parameter(Mandatory)] - [string]$Path, - - [Parameter(Mandatory)] - [ValidateNotNull()] - [object]$Value - ) - - switch ($type = $Value.GetType().Name) { - 'securestring' { $setting = $Value | ConvertFrom-SecureString } - default { $setting = $Value } - } - - if (Test-Path -LiteralPath $Path) { - $storedSettings = Import-Clixml -Path $Path - $storedSettings.Add($Key, @($type, $setting)) - $storedSettings | Export-Clixml -Path $Path - } - else { - $parentDir = Split-Path -Path $Path -Parent - if (!(Test-Path -LiteralPath $parentDir)) { - New-Item $parentDir -ItemType Directory > $null - } - - @{$Key = @($type, $setting)} | Export-Clixml -Path $Path - } -} - -function GetSetting { - param( - [Parameter(Mandatory)] - [string]$Key, - - [Parameter(Mandatory)] - [string]$Path - ) - - if (Test-Path -LiteralPath $Path) { - $securedSettings = Import-Clixml -Path $Path - if ($securedSettings.$Key) { - switch ($securedSettings.$Key[0]) { - 'securestring' { - $value = $securedSettings.$Key[1] | ConvertTo-SecureString - $cred = New-Object -TypeName PSCredential -ArgumentList 'jpgr', $value - $cred.GetNetworkCredential().Password - } - default { - $securedSettings.$Key[1] - } - } - } - } -} - -function SetSetting { - param( - [Parameter(Mandatory)] - [string]$Key, - - [Parameter(Mandatory)] - [string]$Path, - - [Parameter(Mandatory)] - [ValidateNotNull()] - [object]$Value - ) - - if (GetSetting -Key $Key -Path $Path) { - RemoveSetting -Key $Key -Path $Path - } - - AddSetting -Key $Key -Value $Value -Path $Path -} - -function RemoveSetting { - param( - [Parameter(Mandatory)] - [string]$Key, - - [Parameter(Mandatory)] - [string]$Path - ) - - if (Test-Path -LiteralPath $Path) { - $storedSettings = Import-Clixml -Path $Path - $storedSettings.Remove($Key) - if ($storedSettings.Count -eq 0) { - Remove-Item -Path $Path - } - else { - $storedSettings | Export-Clixml -Path $Path - } - } - else { - Write-Warning "The build setting file '$Path' has not been created yet." - } -} diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..8f148d0 --- /dev/null +++ b/cspell.json @@ -0,0 +1,21 @@ +{ + "version": "0.2", + "ignorePaths": [], + "dictionaryDefinitions": [], + "dictionaries": [ + "powershell" + ], + "words": [ + "psake", + "psakefile", + "psakefileps1", + "psbpreference", + "compilemodule", + "defaultlocale", + "testoutputfile", + "frommodule", + "minimumversion" + ], + "ignoreWords": [], + "import": [] +} \ No newline at end of file diff --git a/docs/en-US/Get-ModuleExtension.md b/docs/en-US/Get-ModuleExtension.md new file mode 100644 index 0000000..ed2ab82 --- /dev/null +++ b/docs/en-US/Get-ModuleExtension.md @@ -0,0 +1,106 @@ +--- +external help file: Plaster-help.xml +Module Name: Plaster +online version: +schema: 2.0.0 +--- + +# Get-ModuleExtension + +## SYNOPSIS +{{ Fill in the Synopsis }} + +## SYNTAX + +``` +Get-ModuleExtension [[-ModuleName] ] [[-ModuleVersion] ] [-ListAvailable] + [-ProgressAction ] [] +``` + +## DESCRIPTION +{{ Fill in the Description }} + +## EXAMPLES + +### Example 1 +```powershell +PS C:\> {{ Add example code here }} +``` + +{{ Add example description here }} + +## PARAMETERS + +### -ListAvailable +{{ Fill ListAvailable Description }} + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ModuleName +{{ Fill ModuleName Description }} + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ModuleVersion +{{ Fill ModuleVersion Description }} + +```yaml +Type: Version +Parameter Sets: (All) +Aliases: + +Required: False +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 + +### None + +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/docs/en-US/Get-PlasterTemplate.md b/docs/en-US/Get-PlasterTemplate.md index 97e0be1..ded7b9a 100644 --- a/docs/en-US/Get-PlasterTemplate.md +++ b/docs/en-US/Get-PlasterTemplate.md @@ -15,13 +15,14 @@ cmdlet. ### Path ``` -Get-PlasterTemplate [[-Path] ] [[-Name] ] [-Tag ] [-Recurse] [] +Get-PlasterTemplate [[-Path] ] [[-Name] ] [-Tag ] [-Recurse] + [-ProgressAction ] [] ``` ### IncludedTemplates ``` Get-PlasterTemplate [[-Name] ] [-Tag ] [-IncludeInstalledModules] [-ListAvailable] - [] + [-ProgressAction ] [] ``` ## DESCRIPTION @@ -198,6 +199,21 @@ 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). diff --git a/docs/en-US/Invoke-Plaster.md b/docs/en-US/Invoke-Plaster.md index 94f3ffa..eafb6ae 100644 --- a/docs/en-US/Invoke-Plaster.md +++ b/docs/en-US/Invoke-Plaster.md @@ -13,8 +13,8 @@ Invokes the specified Plaster template which will scaffold out a file or a set o ## SYNTAX ``` -Invoke-Plaster [-TemplatePath] [-DestinationPath] [-Force] [-NoLogo] [-PassThru] [-WhatIf] - [-Confirm] [] +Invoke-Plaster [-TemplatePath] [-DestinationPath] [-Force] [-NoLogo] [-PassThru] + [-ProgressAction ] [-WhatIf] [-Confirm] [] ``` ## DESCRIPTION @@ -162,6 +162,21 @@ 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). diff --git a/docs/en-US/New-PlasterManifest.md b/docs/en-US/New-PlasterManifest.md index a07a1b8..d4c48f1 100644 --- a/docs/en-US/New-PlasterManifest.md +++ b/docs/en-US/New-PlasterManifest.md @@ -15,7 +15,8 @@ Creates a new Plaster template manifest file. ``` New-PlasterManifest [[-Path] ] [-TemplateName] [-TemplateType] [[-Id] ] [[-TemplateVersion] ] [[-Title] ] [[-Description] ] [[-Tags] ] - [[-Author] ] [-AddContent] [-WhatIf] [-Confirm] [] + [[-Author] ] [-AddContent] [-ProgressAction ] [-WhatIf] [-Confirm] + [] ``` ## DESCRIPTION @@ -283,6 +284,21 @@ 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). diff --git a/docs/en-US/Test-PlasterManifest.md b/docs/en-US/Test-PlasterManifest.md index 1be7aff..ad487cd 100644 --- a/docs/en-US/Test-PlasterManifest.md +++ b/docs/en-US/Test-PlasterManifest.md @@ -13,7 +13,7 @@ Verifies that a plaster manifest file is valid. ## SYNTAX ``` -Test-PlasterManifest [[-Path] ] [] +Test-PlasterManifest [[-Path] ] [-ProgressAction ] [] ``` ## DESCRIPTION @@ -57,6 +57,21 @@ Accept pipeline input: True (ByPropertyName, ByValue) 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). diff --git a/psakeFile.ps1 b/psakeFile.ps1 index 7963aa1..9dc3c3e 100644 --- a/psakeFile.ps1 +++ b/psakeFile.ps1 @@ -1,132 +1,19 @@ -properties { - if ($galleryApiKey) { - $PSBPreference.Publish.PSRepositoryApiKey = $galleryApiKey.GetNetworkCredential().password - } - $PSBPreference.Test.OutputFile = 'Out/testResults.xml' - # Using JUnitXML since that can be picked up by github workflow - $PSBPreference.Test.OutputFormat = 'JUnitXml' +Properties { + # Set this to $true to create a module with a monolithic PSM1 + $PSBPreference.Build.CompileModule = $True + $PSBPreference.Build.CompileHeader = @' +#Requires -Version 5.1 +using namespace System.Management.Automation + +# Module initialization +$ErrorActionPreference = 'Stop' +$InformationPreference = 'Continue' + +'@ + $PSBPreference.Help.DefaultLocale = 'en-US' + $PSBPreference.Test.OutputFile = 'out/testResults.xml' } -task default -depends test +Task Default -Depends Test -task Test -FromModule PowerShellBuild -Version 0.6.1 - -Task Sign -depends StageFiles -requiredVariables CertPath, SettingsPath, ScriptSigningEnabled { - if (!$ScriptSigningEnabled) { - "Script signing is not enabled. Skipping $($psake.context.currentTaskName) task." - return - } - - $validCodeSigningCerts = Get-ChildItem -Path $CertPath -CodeSigningCert -Recurse | Where-Object NotAfter -ge (Get-Date) - if (!$validCodeSigningCerts) { - throw "There are no non-expired code-signing certificates in $CertPath. You can either install " + - "a code-signing certificate into the certificate store or disable script analysis in build.settings.ps1." - } - - $certSubjectNameKey = "CertSubjectName" - $storeCertSubjectName = $true - - # Get the subject name of the code-signing certificate to be used for script signing. - if (!$CertSubjectName -and ($CertSubjectName = GetSetting -Key $certSubjectNameKey -Path $SettingsPath)) { - $storeCertSubjectName = $false - } elseif (!$CertSubjectName) { - "A code-signing certificate has not been specified." - "The following non-expired, code-signing certificates are available in your certificate store:" - $validCodeSigningCerts | Format-List Subject, Issuer, Thumbprint, NotBefore, NotAfter - - $CertSubjectName = Read-Host -Prompt 'Enter the subject name (case-sensitive) of the certificate to use for script signing' - } - - # Find a code-signing certificate that matches the specified subject name. - $certificate = $validCodeSigningCerts | - Where-Object { $_.SubjectName.Name -cmatch [regex]::Escape($CertSubjectName) } | - Sort-Object NotAfter -Descending | Select-Object -First 1 - - if ($certificate) { - $SharedProperties.CodeSigningCertificate = $certificate - - if ($storeCertSubjectName) { - SetSetting -Key $certSubjectNameKey -Value $certificate.SubjectName.Name -Path $SettingsPath - "The new certificate subject name has been stored in ${SettingsPath}." - } else { - "Using stored certificate subject name $CertSubjectName from ${SettingsPath}." - } - - $LineSep - "Using code-signing certificate: $certificate" - $LineSep - - $files = @(Get-ChildItem -Path $ModuleOutDir\* -Recurse -Include *.ps1, *.psm1) - foreach ($file in $files) { - $setAuthSigParams = @{ - FilePath = $file.FullName - Certificate = $certificate - Verbose = $VerbosePreference - } - - $result = Microsoft.PowerShell.Security\Set-AuthenticodeSignature @setAuthSigParams - if ($result.Status -ne 'Valid') { - throw "Failed to sign script: $($file.FullName)." - } - - "Successfully signed script: $($file.Name)" - } - } else { - $expiredCert = Get-ChildItem -Path $CertPath -CodeSigningCert -Recurse | - Where-Object { ($_.SubjectName.Name -cmatch [regex]::Escape($CertSubjectName)) -and - ($_.NotAfter -lt (Get-Date)) } - Sort-Object NotAfter -Descending | Select-Object -First 1 - - if ($expiredCert) { - throw "The code-signing certificate `"$($expiredCert.SubjectName.Name)`" EXPIRED on $($expiredCert.NotAfter)." - } - - throw 'No valid certificate subject name supplied or stored.' - } -} - -Task CoreGenerateFileCatalog -requiredVariables CatalogGenerationEnabled, CatalogVersion, ModuleName, ModuleOutDir, OutDir { - if (!$CatalogGenerationEnabled) { - "FileCatalog generation is not enabled. Skipping $($psake.context.currentTaskName) task." - return - } - - if (!(Get-Command Microsoft.PowerShell.Security\New-FileCatalog -ErrorAction SilentlyContinue)) { - "FileCatalog commands not available on this version of PowerShell. Skipping $($psake.context.currentTaskName) task." - return - } - - $catalogFilePath = "$env:BHBuildOutput\$ModuleName.cat" - - $newFileCatalogParams = @{ - Path = $ModuleOutDir - CatalogFilePath = $catalogFilePath - CatalogVersion = $CatalogVersion - Verbose = $VerbosePreference - } - - Microsoft.PowerShell.Security\New-FileCatalog @newFileCatalogParams > $null - - if ($ScriptSigningEnabled) { - if ($SharedProperties.CodeSigningCertificate) { - $setAuthSigParams = @{ - FilePath = $catalogFilePath - Certificate = $SharedProperties.CodeSigningCertificate - Verbose = $VerbosePreference - } - - $result = Microsoft.PowerShell.Security\Set-AuthenticodeSignature @setAuthSigParams - if ($result.Status -ne 'Valid') { - throw "Failed to sign file catalog: $($catalogFilePath)." - } - - "Successfully signed file catalog: $($catalogFilePath)" - } else { - "No code-signing certificate was found to sign the file catalog." - } - } else { - "Script signing is not enabled. Skipping signing of file catalog." - } - - Move-Item -LiteralPath $newFileCatalogParams.CatalogFilePath -Destination $ModuleOutDir -} +Task Test -FromModule PowerShellBuild -MinimumVersion '0.6.1' \ No newline at end of file diff --git a/requirements.psd1 b/requirements.psd1 new file mode 100644 index 0000000..50a294e --- /dev/null +++ b/requirements.psd1 @@ -0,0 +1,26 @@ +@{ + PSDepend = @{ + Version = '0.3.8' + } + PSDependOptions = @{ + Target = 'CurrentUser' + } + 'Pester' = @{ + Version = '5.7.1' + Parameters = @{ + SkipPublisherCheck = $true + } + } + 'psake' = @{ + Version = '4.9.1' + } + 'BuildHelpers' = @{ + Version = '2.0.16' + } + 'PowerShellBuild' = @{ + Version = '0.7.2' + } + 'PSScriptAnalyzer' = @{ + Version = '1.24.0' + } +} \ No newline at end of file From 223cc90b1b05e2e65addc4bb561141e43e68cc94 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 13 Jul 2025 19:15:44 -0700 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20=E2=9C=A8=20Update=20GitHub=20Acti?= =?UTF-8?q?ons=20workflow=20and=20spell-check=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated `actions/checkout` to version 4 * Added caching for PowerShell modules using `psmodulecache` * Improved module dependency handling in the workflow * Added `plaster` to the spell-checker ignore list in `cspell.json` --- .github/workflows/PesterReports.yml | 36 ++++++++++++++++++----------- Plaster/Plaster.psm1 | 2 +- cspell.json | 3 ++- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/PesterReports.yml b/.github/workflows/PesterReports.yml index 6108dc7..7c2b214 100644 --- a/.github/workflows/PesterReports.yml +++ b/.github/workflows/PesterReports.yml @@ -1,3 +1,4 @@ +# spell-checker:ignore potatoqualitee psdepend psmodulecache name: PesterReports # Controls when the action will run. on: @@ -5,8 +6,6 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -21,25 +20,34 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] # Steps represent a sequence of tasks that will be executed as part of the job steps: - - uses: actions/checkout@v2 - # Setup Build Helpers - - name: SetupBuildHelpers + - uses: actions/checkout@v4 + - name: Install and cache PSDepend + id: psdepend + uses: potatoqualitee/psmodulecache@v6.2.1 + with: + modules-to-cache: PSDepend:0.3.8 + - name: Determine modules to cache shell: pwsh + id: modules-to-cache run: | - Install-Module BuildHelpers -Scope CurrentUser -Force | Out-Null - Install-Module PowerShellBuild -Scope CurrentUser -Force | Out-Null - Install-Module PSScriptAnalyzer -Scope CurrentUser -Force | Out-Null - Install-Module platyPS -Scope CurrentUser -Force | Out-Null + $dependencies = Get-Dependency + $f = $dependencies | ?{ $_.DependencyType -eq 'PSGalleryModule' } | %{ "{0}:{1}" -F $_.DependencyName, $_.Version} + Write-Output "::set-output name=ModulesToCache::$($f -join ', ')" + - name: Install and cache PowerShell modules + id: psmodulecache + uses: potatoqualitee/psmodulecache@v6.2.1 + with: + modules-to-cache: ${{ steps.modules-to-cache.outputs.ModulesToCache }} + shell: pwsh - name: Test shell: pwsh - run: | - ./build.ps1 -Task Test + run: ./build.ps1 -Task Test - name: Upload Unit Test Results if: always() - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Unit Test Results (OS ${{ matrix.os }}) - path: ./tests/Out/testResults.xml + path: ./tests/out/testResults.xml publish-test-results: name: "Publish Unit Tests Results" @@ -55,6 +63,6 @@ jobs: path: artifacts - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action@v1 + uses: EnricoMi/publish-unit-test-result-action@v2 with: files: artifacts/**/*.xml diff --git a/Plaster/Plaster.psm1 b/Plaster/Plaster.psm1 index 0e5aafd..060b2cb 100644 --- a/Plaster/Plaster.psm1 +++ b/Plaster/Plaster.psm1 @@ -1,4 +1,4 @@ - +# spell-checker:ignore Multichoice Assigments # Import localized data data LocalizedData { # culture="en-US" diff --git a/cspell.json b/cspell.json index 8f148d0..800782d 100644 --- a/cspell.json +++ b/cspell.json @@ -14,7 +14,8 @@ "defaultlocale", "testoutputfile", "frommodule", - "minimumversion" + "minimumversion", + "plaster", ], "ignoreWords": [], "import": [] From 1d9974064c13649185e8bd07d06e046ab9f4b086 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 13 Jul 2025 19:20:36 -0700 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=E2=9C=A8=20Update=20permissions?= =?UTF-8?q?=20and=20action=20versions=20in=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added `permissions` for `checks` and `issues` in the workflow. * Updated `actions/upload-artifact` to version `v4.6.2`. * Updated `actions/download-artifact` to version `v4.3.0`. * Updated `EnricoMi/publish-unit-test-result-action` to version `v2.20.0`. --- .github/workflows/PesterReports.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/PesterReports.yml b/.github/workflows/PesterReports.yml index 7c2b214..2b89b80 100644 --- a/.github/workflows/PesterReports.yml +++ b/.github/workflows/PesterReports.yml @@ -8,7 +8,9 @@ on: pull_request: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: - +permissions: + checks: write + issues: write # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" @@ -44,7 +46,7 @@ jobs: run: ./build.ps1 -Task Test - name: Upload Unit Test Results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.6.2 with: name: Unit Test Results (OS ${{ matrix.os }}) path: ./tests/out/testResults.xml @@ -58,11 +60,11 @@ jobs: steps: - name: Download Artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4.3.0 with: path: artifacts - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 + uses: EnricoMi/publish-unit-test-result-action@v2.20.0 with: files: artifacts/**/*.xml From d44479a5abc01449679632a8b076daaa6214efbd Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sun, 13 Jul 2025 19:24:04 -0700 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20=E2=9C=A8=20Update=20permissions?= =?UTF-8?q?=20in=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changed `issues: write` to `pull-requests: write` for better clarity on permissions. * Updated condition for the `publish-test-results` job to `if: (!cancelled())` for improved control flow. --- .github/workflows/PesterReports.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/PesterReports.yml b/.github/workflows/PesterReports.yml index 2b89b80..e0de548 100644 --- a/.github/workflows/PesterReports.yml +++ b/.github/workflows/PesterReports.yml @@ -10,7 +10,8 @@ on: workflow_dispatch: permissions: checks: write - issues: write + pull-requests: write + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" @@ -56,7 +57,7 @@ jobs: needs: test runs-on: ubuntu-latest # the test job might be skipped, we don't need to run this job then - if: success() || failure() + if: (!cancelled()) steps: - name: Download Artifacts