diff --git a/PSModuleDevelopment/PSModuleDevelopment.psd1 b/PSModuleDevelopment/PSModuleDevelopment.psd1 index af8c7bb..630f8e0 100644 --- a/PSModuleDevelopment/PSModuleDevelopment.psd1 +++ b/PSModuleDevelopment/PSModuleDevelopment.psd1 @@ -4,7 +4,7 @@ RootModule = 'PSModuleDevelopment.psm1' # Version number of this module. - ModuleVersion = '2.2.9.107' + ModuleVersion = '2.2.10.120' # ID used to uniquely identify this module GUID = '37dd5fce-e7b5-4d57-ac37-832055ce49d6' @@ -27,7 +27,7 @@ # Modules that must be imported into the global environment prior to importing # this module RequiredModules = @( - @{ ModuleName = 'PSFramework'; ModuleVersion = '1.4.149' } + @{ ModuleName = 'PSFramework'; ModuleVersion = '1.6.198' } @{ ModuleName = 'string'; ModuleVersion = '0.6.1' } ) @@ -50,45 +50,57 @@ # Functions to export from this module FunctionsToExport = @( - 'Convert-PSMDMessage', - 'Expand-PSMDTypeName', - 'Export-PSMDString', - 'Find-PSMDFileContent', - 'Find-PSMDType', - 'Format-PSMDParameter', - 'Get-PSMDArgumentCompleter', - 'Get-PSMDAssembly', - 'Get-PSMDConstructor', - 'Get-PSMDFileCommand', - 'Get-PSMDHelp', - 'Get-PSMDMember', - 'Get-PSMDModuleDebug', - 'Get-PSMDTemplate', - 'Import-PSMDModuleDebug', - 'Invoke-PSMDTemplate', - 'Measure-PSMDCommand', - 'Measure-PSMDLinesOfCode', - 'New-PSMDDotNetProject', - 'New-PSMDHeader', - 'New-PSMDFormatTableDefinition', - 'New-PSMDModuleNugetPackage', - 'New-PSMDTemplate', - 'New-PssModuleProject', - 'Publish-PSMDScriptFile', - 'Publish-PSMDStagedModule', - 'Read-PSMDScript', - 'Remove-PSMDModuleDebug', - 'Remove-PSMDTemplate', - 'Rename-PSMDParameter', - 'Restart-PSMDShell', - 'Search-PSMDPropertyValue', - 'Set-PSMDEncoding', - 'Set-PSMDModuleDebug', - 'Set-PSMDCmdletBinding', - 'Set-PSMDModulePath', - 'Set-PSMDParameterHelp', - 'Set-PSMDStagingRepository', - 'Show-PSMDSyntax', + 'Convert-PSMDMessage' + 'Expand-PSMDTypeName' + 'Export-PSMDString' + 'Find-PSMDFileContent' + 'Find-PSMDType' + 'Format-PSMDParameter' + 'Get-PSMDArgumentCompleter' + 'Get-PSMDAssembly' + 'Get-PSMDBuildAction' + 'Get-PSMDBuildArtifact' + 'Get-PSMDBuildProject' + 'Get-PSMDBuildStep' + 'Get-PSMDConstructor' + 'Get-PSMDFileCommand' + 'Get-PSMDHelp' + 'Get-PSMDMember' + 'Get-PSMDModuleDebug' + 'Get-PSMDTemplate' + 'Import-PSMDModuleDebug' + 'Invoke-PSMDBuildProject' + 'Invoke-PSMDTemplate' + 'Measure-PSMDCommand' + 'Measure-PSMDLinesOfCode' + 'New-PSMDBuildProject' + 'New-PSMDDotNetProject' + 'New-PSMDFormatTableDefinition' + 'New-PSMDHeader' + 'New-PSMDModuleNugetPackage' + 'New-PSMDTemplate' + 'New-PssModuleProject' + 'Publish-PSMDBuildArtifact' + 'Publish-PSMDScriptFile' + 'Publish-PSMDStagedModule' + 'Read-PSMDScript' + 'Register-PSMDBuildAction' + 'Remove-PSMDBuildArtifact' + 'Remove-PSMDModuleDebug' + 'Remove-PSMDTemplate' + 'Rename-PSMDParameter' + 'Resolve-PSMDBuildStepParameter' + 'Restart-PSMDShell' + 'Search-PSMDPropertyValue' + 'Select-PSMDBuildProject' + 'Set-PSMDBuildStep' + 'Set-PSMDCmdletBinding' + 'Set-PSMDEncoding' + 'Set-PSMDModuleDebug' + 'Set-PSMDModulePath' + 'Set-PSMDParameterHelp' + 'Set-PSMDStagingRepository' + 'Show-PSMDSyntax' 'Split-PSMDScriptFile' ) @@ -99,15 +111,16 @@ # VariablesToExport = '' # Aliases to export from this module - AliasesToExport = @( - 'dotnetnew', - 'find', - 'hex', - 'imt', - 'ipmod', - 'parse', - 'Restart-Shell', - 'rss', + AliasesToExport = @( + 'build' + 'dotnetnew' + 'find' + 'hex' + 'imt' + 'ipmod' + 'parse' + 'Restart-Shell' + 'rss' 'smd' ) diff --git a/PSModuleDevelopment/bin/PSModuleDevelopment.dll b/PSModuleDevelopment/bin/PSModuleDevelopment.dll index 4e23017..4d4a049 100644 Binary files a/PSModuleDevelopment/bin/PSModuleDevelopment.dll and b/PSModuleDevelopment/bin/PSModuleDevelopment.dll differ diff --git a/PSModuleDevelopment/bin/PSModuleDevelopment.pdb b/PSModuleDevelopment/bin/PSModuleDevelopment.pdb index e84f1e7..68f03cb 100644 Binary files a/PSModuleDevelopment/bin/PSModuleDevelopment.pdb and b/PSModuleDevelopment/bin/PSModuleDevelopment.pdb differ diff --git a/PSModuleDevelopment/bin/PSModuleDevelopment.xml b/PSModuleDevelopment/bin/PSModuleDevelopment.xml index 2f778e5..cc60b59 100644 --- a/PSModuleDevelopment/bin/PSModuleDevelopment.xml +++ b/PSModuleDevelopment/bin/PSModuleDevelopment.xml @@ -282,6 +282,141 @@ The constructor info object describing the constructor + + + Base class for all kinds of parameters gen 2+ + + + + + Name of the parameter + + + + + Description of the parameter + + + + + Get the value associated with this parameter + + The value to insert into the artifact generated from the template + + + + A template parameter where the user is prompted for input. + + + + + The value provided by the user + + + + + List of legal values to provide + + + + + A validation pattern that needs to be met. + + + + + An error description that will be shown if the user provides invalid input to a parameter with pattern validation. + + + + + Test whether the input meets the validation rules + + The value to test + Whether the value is valid. + + + + Return the value specified by the user. + + The value specified by the user + + + + Parameter type executing + + + + + The scriptblock to execute. + Wrapped as string for serialization purposes. + + + + + The value of the scriptblock. + Populated by the GetValue() method usually called with the "StartUp" timing. + + + + + When exactly during the template process should this scriptblock be executed? + + + + + Setting this to true will cause the Invoke-PSMDTemplate command to omit inserting values for the + + + + + Returns the string value of the scriptblock by executing it! + + The string value of the scriptblock by executing it! + + + + Execute the scriptblock "Just-in-time" during either PreItemCreation or PostItemCreation Timing. + + The file/directory info object of the object recently or about to be created + Returns a string value resulting from the scriptblock to insert + + + + Execute the scriptblock "Just-in-time" during either PreItemCreation or PostItemCreation Timing. + + The path to the file/directory info object of the object recently or about to be created + Whether the object (about to be) created is a file. + Returns a string value resulting from the scriptblock to insert + + + + When will a specific scriptblock parameter be executed? + + + + + Executed when starting the overall template invocation + + + + + Executed before an individual item using it is created. + Values will be inserted into the file-content before writing to disk if applicable. + + + + + Executed after the individual item using it has been created. + Output will be discarded, but scriptblock will receive path of file / folder. + + + + + Executed after the entire project has been written. + Enables post-processing. + + A script used to calculate content to be inserted @@ -395,11 +530,21 @@ List of scripts that will be invoked on initialization + + + List of generation 2 parameters to include + + Items in the root directory of the template (which may contain children themselves). + + + What design generation is the template? + + Returns the template digest used as index file. @@ -461,6 +606,11 @@ The path to the template file + + + What template generation is this file? + + The version-qualified name of the template diff --git a/PSModuleDevelopment/changelog.md b/PSModuleDevelopment/changelog.md index da0083f..c55f4fa 100644 --- a/PSModuleDevelopment/changelog.md +++ b/PSModuleDevelopment/changelog.md @@ -1,11 +1,27 @@ # Changelog +## 2.2.10.120 (2021-07-20) + +- New: Build Component - define build workflows based on pre-defined & extensible action code +- Upd: Template AzureFunction - new layout with better build automation +- Upd: Template AzureFunctionRest - new layout to integrate into new AzureFunction template +- Upd: Template PSFProject - added Github Actions integration +- Upd: Aliases - removed "AllScope" option +- Fix: Template PSFTest - fixed PSScriptAnalyzer test path detection +- Fix: Template PSFTest - fixed string LegalSurplus exception being ignored +- Fix: Template PSFModule - fixed PSScriptAnalyzer test path detection +- Fix: Template PSFModule - fixed string LegalSurplus exception being ignored +- Fix: Template PSFProject - fixed PSScriptAnalyzer test path detection +- Fix: Template PSFProject - fixed string LegalSurplus exception being ignored +- Fix: TemplateStore - default path iss invalid on MAC (#136) +- Fix: Invoke-PSMDTemplate - unreliable string replacement through -replace operator (#113) +- Fix: Publish-PSMDScriptFile - insufficient exclude paths (#138; @Callidus2000) + ## 2.2.9.106 (September 10th, 2020) - New: Convert-PSMDMessage - Converts a file's use of PSFramework messages to strings. - Upd: Export-PSMDString - Adding support for Test-PSFShouldProcess. - Fix: Export-PSMDString - Failed with splatting detection -- Fix: Publish-PSMDScriptFile - insufficient exclude paths (#138; @Callidus2000) ## 2.2.8.104 (July 26th, 2020) diff --git a/PSModuleDevelopment/en-us/strings.psd1 b/PSModuleDevelopment/en-us/strings.psd1 index 65c294e..e8c66ff 100644 --- a/PSModuleDevelopment/en-us/strings.psd1 +++ b/PSModuleDevelopment/en-us/strings.psd1 @@ -4,6 +4,11 @@ 'Get-PSMDFileCommand.SyntaxError' = 'Syntax error in file: {0}' # $pathItem + 'Invoke-PSMDBuildProject.Step.Executing' = '[{0}] Executing step {1} ({2})' # $count, $step.Name, $step.Action + + 'Invoke-PSMDTemplate.Template.NotFound' = 'Unable to find template data for "{0}"' # $TemplateName + 'Invoke-PSMDTemplate.Invoking' = 'Creating file/project from template {0}' # $item + 'MeasurePSMDLinesOfCode.Processing' = 'Processing Path: {0}' # $fileItem 'Publish-PSMDScriptFile.Module.Saving' = 'Saving module {0} from repository {1}' # $moduleName, (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Script.StagingRepository') diff --git a/PSModuleDevelopment/functions/build/Get-PSMDBuildAction.ps1 b/PSModuleDevelopment/functions/build/Get-PSMDBuildAction.ps1 new file mode 100644 index 0000000..6ff5b48 --- /dev/null +++ b/PSModuleDevelopment/functions/build/Get-PSMDBuildAction.ps1 @@ -0,0 +1,29 @@ +function Get-PSMDBuildAction { +<# + .SYNOPSIS + Get a list of registered build actions. + + .DESCRIPTION + Get a list of registered build actions. + Actions are the scriptblocks that are used to execute the build logic when running Invoke-PSMDBuildProject. + + .PARAMETER Name + The name by which to filter the actions returned. + Defaults to '*' + + .EXAMPLE + PS C:\> Get-PSMDBuildAction + + Get a list of all registered build actions. +#> + [CmdletBinding()] + param ( + [PsfArgumentCompleter('PSModuleDevelopment.Build.Action')] + [string] + $Name = '*' + ) + + process { + $script:buildActions.Values | Where-Object Name -Like $Name + } +} diff --git a/PSModuleDevelopment/functions/build/Get-PSMDBuildArtifact.ps1 b/PSModuleDevelopment/functions/build/Get-PSMDBuildArtifact.ps1 new file mode 100644 index 0000000..011accc --- /dev/null +++ b/PSModuleDevelopment/functions/build/Get-PSMDBuildArtifact.ps1 @@ -0,0 +1,54 @@ +function Get-PSMDBuildArtifact { +<# + .SYNOPSIS + Retrieve an artifact during a build project's execution. + + .DESCRIPTION + Retrieve an artifact during a build project's execution. + These artifacts are usually created during such an execution and discarded once completed. + + .PARAMETER Name + The name by which to search for artifacts. + Defaults to '*' + + .PARAMETER Tag + Search for artifacts by tag. + Artifacts can receive tag for better categorization. + When specifying multiple tags, any artifact containing at least one of them will be returned. + + .EXAMPLE + PS C:\> Get-PSMDBuildArtifact + + List all available artifacts. + + .EXAMPLE + PS C:\> Get-PSMDBuildArtifact -Name ReleasePath + + Returns the artifact named "ReleasePath" + + .EXAMPLE + PS C:\> Get-PSMDBuildArtifact -Tag pssession + + Returns all artifacts with the tag "pssession" +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] + [CmdletBinding()] + param ( + [string] + $Name = '*', + + [string[]] + $Tag + ) + + process { + $artifacts = $script:buildArtifacts.Values | Where-Object Name -Like $Name | Where-Object { + if (-not $Tag) { return $true } + foreach ($tagName in $Tag) { + if ($_.Tags -contains $Tag) { return $true } + } + return $false + } + $($artifacts) + } +} diff --git a/PSModuleDevelopment/functions/build/Get-PSMDBuildProject.ps1 b/PSModuleDevelopment/functions/build/Get-PSMDBuildProject.ps1 new file mode 100644 index 0000000..93a7594 --- /dev/null +++ b/PSModuleDevelopment/functions/build/Get-PSMDBuildProject.ps1 @@ -0,0 +1,60 @@ +function Get-PSMDBuildProject { +<# + .SYNOPSIS + Reads & returns a build project. + + .DESCRIPTION + Reads & returns a build project. + A build project is a container including the steps executed during the build. + + .PARAMETER Path + Path to the build project file. + May target the folder, in which case the -Name parameter must be specified. + + .PARAMETER Name + The name of the build project to read. + Use together with the -Path parameter only. + Absolute file path assumed will be: "\.build.json" + + .PARAMETER Selected + Rather than specifying the path to read from, return the currently selected build project. + Use Select-PSMDBuildProject to select a build project as the default ("selected") project. + + .EXAMPLE + PS C:\> Get-PSMDBuildProject -Path 'C:\code\project' -Name project + + Will load the build project stored in the file "C:\code\project\project.build.json" +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] + [CmdletBinding(DefaultParameterSetName = 'Path')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Path')] + [string] + $Path, + + [Parameter(ParameterSetName = 'Path')] + [string] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Selected')] + [switch] + $Selected + ) + + process { + #region By Path + if ($Path) { + $importPath = $Path + if ($Name) { $importPath = Join-Path -Path $Path -ChildPath "$Name.build.json" } + + Get-Content -Path $importPath -Encoding UTF8 | ConvertFrom-Json + } + #endregion By Path + + #region Selected + else { + Get-Content -Path (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected') -Encoding UTF8 | ConvertFrom-Json + } + #endregion Selected + } +} diff --git a/PSModuleDevelopment/functions/build/Get-PSMDBuildStep.ps1 b/PSModuleDevelopment/functions/build/Get-PSMDBuildStep.ps1 new file mode 100644 index 0000000..7c9890e --- /dev/null +++ b/PSModuleDevelopment/functions/build/Get-PSMDBuildStep.ps1 @@ -0,0 +1,49 @@ +function Get-PSMDBuildStep { +<# + .SYNOPSIS + Read the steps that are part of the specified build project. + + .DESCRIPTION + Read the steps that are part of the specified build project. + + .PARAMETER Name + The name by which to filter the steps returned. + Defaults to '*' + + .PARAMETER BuildProject + Path to the build project file to read from. + Defaults to the currently selected project if available. + Use Select-PSMDBuildProject to select a default project. + + .EXAMPLE + PS C:\> Get-PSMDBuildStep + + Read all steps that are part of the default build project. + + .EXAMPLE + PS C:\> Get-PSMDBuildStep -Name CreateSession -BuildProject C:\code\Project\Project.build.json + + Return the CreateSession step from the specified project file. +#> + [CmdletBinding()] + param ( + [string] + $Name = '*', + + [string] + $BuildProject + ) + + begin { + $projectPath = $BuildProject + if (-not $projectPath) { $projectPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' } + if (-not $projectPath) { throw "No Project path specified and none selected!" } + if (-not (Test-Path -Path $projectPath)) { + throw "Project file not found: $projectPath" + } + } + process { + $projectObject = Get-PSMDBuildProject -Path $projectPath + $projectObject.Steps | Where-Object Name -Like $Name + } +} diff --git a/PSModuleDevelopment/functions/build/Invoke-PSMDBuildProject.ps1 b/PSModuleDevelopment/functions/build/Invoke-PSMDBuildProject.ps1 new file mode 100644 index 0000000..ee8567b --- /dev/null +++ b/PSModuleDevelopment/functions/build/Invoke-PSMDBuildProject.ps1 @@ -0,0 +1,175 @@ +function Invoke-PSMDBuildProject { +<# + .SYNOPSIS + Execute a build project. + + .DESCRIPTION + Execute a build project. + A build project is a configured chain of actions that have been configured in json. + They will be processed in their specified order and allow manageable, configurable steps without having to reinvent the same action again and again. + + + Individual action types become available using Register-PSMDBuildAction. + + Create new build projects using New-PSMDBuildProject + + Set up steps taken during a build using Set-PSMDBuildStep + + Select the default build project using Select-PSMDBuildProject + + .PARAMETER Path + The path to the build project file to execute. + Mandatory if no build project has been selected as the default project. + Use the Select-PSMDBuildProject to define a default project (and optionally persist the choice across sessions) + + .PARAMETER RetainArtifacts + Whether, after executing the project, its artifacts should be retained. + By default, any artifacts created during a build project will be discarded upon project completion. + + Artifacts are similar to variables to the pipeline and can be used to pass data throughout the pipeline. + + + Use Publish-PSMDBuildArtifact to create a new artifact. + + Use Get-PSMDBuildArtifact to access existing build artifacts. + + .EXAMPLE + PS C:\> Invoke-PSMDBuildProject -Path .\VMDeployment.build.Json + + Execute the build file "VMDeployment.build.json" from the current folder + + .EXAMPLE + PS C:\> build + + Execute the default build project. +#> + [Alias('build')] + [CmdletBinding()] + param ( + [string] + $Path, + + [switch] + $RetainArtifacts + ) + + begin { + $script:buildArtifacts = @{ } + $buildStatus = @{ } + + $projectPath = $Path + if (-not $projectPath) { $projectPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' } + if (-not $projectPath) { throw "No Project path specified and none selected!" } + if (-not (Test-Path -Path $projectPath)) { + throw "Project file not found: $projectPath" + } + + function Write-StepResult { + [CmdletBinding()] + param ( + [int] + $Count, + + [ValidateSet('Success', 'Failed', 'ConditionNotMet', 'DependencyNotMet', 'BadAction')] + [string] + $Status, + + $StepObject, + + $Data, + + [hashtable] + $BuildStatus, + + [string] + $ContinueLabel + ) + + $BuildStatus[$StepObject.Name] = $Status -eq 'Success' + + $paramWritePSFMessage = @{ + Level = 'Warning' + String = "Invoke-PSMDBuildProject.Step.$Status" + } + + switch ($Status) { + Failed { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action -ErrorRecord $Data } + ConditionNotMet { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action, $StepObject.Condition } + DependencyNotMet { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action, $Data } + BadAction { Write-PSFMessage @paramWritePSFMessage -StringValues $StepObject.Name, $StepObject.Action } + } + + [PSCustomObject]@{ + PSTypeName = 'PSModuleDevelopment.Build.StepResult' + Count = $Count + Action = $StepObject.Action + Status = $Status + Step = $StepObject.Name + Data = $Data + } + + if ($ContinueLabel) { + continue $ContinueLabel + } + } + } + process { + $projectObject = Get-PSMDBuildProject -Path $projectPath + $steps = $projectObject.Steps | Sort-Object Weight + + $count = 0 + $stepResults = :main foreach ($step in $steps) { + $count++ + $resultDef = @{ + Count = $count + StepObject = $step + BuildStatus = $buildStatus + } + + Write-PSFMessage -Level Host -String 'Invoke-PSMDBuildProject.Step.Executing' -StringValues $count, $step.Name, $step.Action + + #region Validation + $actionObject = $script:buildActions[$step.Action] + if (-not $actionObject) { + Write-StepResult @resultDef -Status BadAction -ContinueLabel main + } + + foreach ($dependency in $step.Dependency) { + if (-not $buildStatus[$dependency]) { + Write-StepResult @resultDef -Status DependencyNotMet -Data $dependency -ContinueLabel main + } + } + + if ($step.Condition -and $step.ConditionSet) { + $cModule, $cSetName = $step.ConditionSet -split " ", 2 + $conditionSet = Get-PSFFilterConditionSet -Module $cModule -Name $cSetName + if (-not $conditionSet) { + Write-StepResult @resultDef -Status ConditionNotMet -ContinueLabel main + } + + $filter = New-PSFFilter -Expression $step.Condition -ConditionSet $conditionSet + if (-not $filter.Evaluate()) { + Write-StepResult @resultDef -Status ConditionNotMet -ContinueLabel main + } + } + #endregion Validation + + #region Execution + $parameters = @{ + RootPath = Split-Path -Path $projectPath + Parameters = $step.Parameters | ConvertTo-PSFHashtable + ProjectName = $projectObject.Name + StepName = $step.Name + ParametersFromArtifacts = $step.ParametersFromArtifacts | ConvertTo-PSFHashtable + } + if (-not $parameters.Parameters) { $parameters.Parameters = @{ } } + if (-not $parameters.ParametersFromArtifacts) { $parameters.ParametersFromArtifacts = @{ } } + try { $null = & $actionObject.Action $parameters } + catch { + Write-StepResult @resultDef -Status Failed -Data $_ -ContinueLabel main + } + Write-StepResult @resultDef -Status Success + #endregion Execution + } + $stepResults + } + end { + if (-not $RetainArtifacts) { + $script:buildArtifacts = @{ } + } + } +} diff --git a/PSModuleDevelopment/functions/build/New-PSMDBuildProject.ps1 b/PSModuleDevelopment/functions/build/New-PSMDBuildProject.ps1 new file mode 100644 index 0000000..6d44498 --- /dev/null +++ b/PSModuleDevelopment/functions/build/New-PSMDBuildProject.ps1 @@ -0,0 +1,94 @@ +function New-PSMDBuildProject { +<# + .SYNOPSIS + Create a new build project file. + + .DESCRIPTION + Create a new build project file. + Build projects are used to configure a repeatable, managed set of steps that make up a workflow. + It is designed with software build processes in mind, but can be used for pretty much anything that works in separate steps. + + See the help on Invoke-PSMDBuildProject for more details. + + NOTE: This is not the tool or component to create new PowerShell _code_ projects / repositories! + To create a new PowerShell module project, instead run: + + Invoke-PSMDTemplate PSFProject + + .PARAMETER Name + The name of the build project. + + .PARAMETER Path + The path to the folder in which the build project file is created. + Final path will be: "\.build.json" + + .PARAMETER Condition + A condition - a filter expression - that must be met in order for the build to proceed. + For more details on filter conditions, see the PSFramework documentation on its feature: + https://psframework.org/documentation/documents/psframework/filters.html + + .PARAMETER ConditionSet + The name of the condition set to use. + This is part of the PSFramework filter system: + https://psframework.org/documentation/documents/psframework/filters.html + + Specify as " " format. + Default Value: PSFramework Environment + + .PARAMETER NoSelect + Do not select the newly created build project as the default project for the current session. + By default, the newly created build project will be set as default project, in order to facilitate adding steps to it. + Use Select-PSMDBuildProject to explicitly set a default project file. + + .PARAMETER Register + Persist the newly created build project as default build project beyond the current session. + By default, the newly created build project will already be set as default project, in order to facilitate adding steps to it. + But ONLY for the current session. This parameter makes it remember in new PowerShell sessions as well. + + .EXAMPLE + PS C:\> New-PSMDBuildProject -Name 'VMDeployment' -Path 'C:\Code\VMDeployment' + + Create a new build project named 'VMDeployment' in the folder 'C:\Code\VMDeployment' +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding(DefaultParameterSetName = 'default')] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')] + [string] + $Path, + + [string] + $Condition, + + [string] + $ConditionSet = 'PSFramework Environment', + + [Parameter(ParameterSetName = 'NoSelect')] + [switch] + $NoSelect, + + [Parameter(ParameterSetName = 'Register')] + [switch] + $Register + ) + + process { + $project = [pscustomobject]@{ + Name = $Name + Condition = $Condition + ConditionSet = $ConditionSet + Steps = @() + } + $outPath = Join-Path -Path $Path -ChildPath "$Name.build.Json" + $project | Export-PsmdBuildProjectFile -OutPath $outPath -ErrorAction Stop + if (-not $NoSelect) { + Set-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' -Value $outPath + if ($Register) { Register-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' } + } + } +} diff --git a/PSModuleDevelopment/functions/build/Publish-PSMDBuildArtifact.ps1 b/PSModuleDevelopment/functions/build/Publish-PSMDBuildArtifact.ps1 new file mode 100644 index 0000000..6223327 --- /dev/null +++ b/PSModuleDevelopment/functions/build/Publish-PSMDBuildArtifact.ps1 @@ -0,0 +1,54 @@ +function Publish-PSMDBuildArtifact { +<# + .SYNOPSIS + Create a new artifact for the current build pipeline. + + .DESCRIPTION + Create a new artifact for the current build pipeline. + Use this create artifacts that are accessible in later steps in the pipeline. + + Usually, artifacts are deleted at the end of a build process. + They are always cleared at the beginning of a new one. + + Artifacts are NOT persisted across PowerShell sessions. + + .PARAMETER Name + Name of the Artifact to create. + Technically there are no limits to which character to chose, but we strongly encourage restricting yourself to letters, numbers, dash, underscore and dot. + + .PARAMETER Value + The value to assign to the artifact. + + .PARAMETER Tag + Any tags to add to an artifact. + Tags can be searched for in order to bulk-operate against all artifacts of that tag. + For example, the "remove-pssession" action can remove all remoting sessions for all artifacts tagged as "pssession". + + .EXAMPLE + PS C:\> Publish-PSMDBuildArtifact -Name 'session' -Value $session -Tag 'pssession' + + Publishes an artifact named "session" containing the content of $session that is tagged as a PowerShell remoting session. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [AllowNull()] + $Value, + + [string[]] + $Tag = @() + ) + + process { + $script:buildArtifacts[$Name] = [pscustomobject]@{ + PSTypeName = 'PSModuleDevelopment.Build.Artifact' + Name = $Name + Value = $Value + Tags = $Tag + } + } +} diff --git a/PSModuleDevelopment/functions/build/Register-PSMDBuildAction.ps1 b/PSModuleDevelopment/functions/build/Register-PSMDBuildAction.ps1 new file mode 100644 index 0000000..8728f03 --- /dev/null +++ b/PSModuleDevelopment/functions/build/Register-PSMDBuildAction.ps1 @@ -0,0 +1,62 @@ +function Register-PSMDBuildAction { +<# + .SYNOPSIS + Register a new action usable in build projects. + + .DESCRIPTION + Register a new action usable in build projects. + Actions are the actual implementation logic that turns the configuration in a build project file into ... well, actions. + Anyway, these are basically named scriptblocks with some metadata. + This command is used to provide all the builtin actions and can be used to freely define your own actions. + + Whenever you use a "script" action in your build projects, consider ... would it make a good configurable option valuable for other builds? + If so, that might just mark the birth of the next action! + + .PARAMETER Name + The name of the action. + + .PARAMETER Action + The actual code implementing the action. + Each action scriptblock will receive exactly one . + + .PARAMETER Description + A description explaining what the action is all about. + + .PARAMETER Parameters + The parameters the action accepts. + Provider a hashtable, with the keys being the parameter names and the values being a description of its parameter. + + .EXAMPLE + PS C:\> Register-PSMDBuildAction -Name 'script' -Action $actionCode -Description 'Execute a custom scriptfile as part of your workflow' -Parameters $parameters + + Creates / registers the action "script". +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [ScriptBlock] + $Action, + + [Parameter(Mandatory = $true)] + [string] + $Description, + + [Parameter(Mandatory = $true)] + [hashtable] + $Parameters + ) + + process { + $script:buildActions[$Name] = [pscustomobject]@{ + PSTypeName = 'PSModuleDevelopment.Build.Action' + Name = $Name + Action = $Action + Description = $Description + Parameters = $Parameters + } + } +} diff --git a/PSModuleDevelopment/functions/build/Remove-PSMDBuildArtifact.ps1 b/PSModuleDevelopment/functions/build/Remove-PSMDBuildArtifact.ps1 new file mode 100644 index 0000000..a6c354b --- /dev/null +++ b/PSModuleDevelopment/functions/build/Remove-PSMDBuildArtifact.ps1 @@ -0,0 +1,37 @@ +function Remove-PSMDBuildArtifact +{ +<# + .SYNOPSIS + Removes an artifact from the build pipeline. + + .DESCRIPTION + Removes an artifact from the build pipeline. + Only interacts with the PSModuleDevelopment build system. + + .PARAMETER Name + Name of the artifact to remove. + + .EXAMPLE + PS C:\> Remove-PSMDBuildArtifact -Name 'session' + + Removes the artifact 'session' from the build pipeline. + + .EXAMPLE + PS C:\> Get-PSMDBuildArtifact -Tag pssession | Remove-PSMDBuildArtifact + + Removes all artifacts with the 'pssession' tag from the build pipeline. +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [string[]] + $Name + ) + + process{ + foreach ($nameString in $Name) { + $script:buildArtifacts.Remove($nameString) + } + } +} diff --git a/PSModuleDevelopment/functions/build/Resolve-PSMDBuildStepParameter.ps1 b/PSModuleDevelopment/functions/build/Resolve-PSMDBuildStepParameter.ps1 new file mode 100644 index 0000000..82c7ffc --- /dev/null +++ b/PSModuleDevelopment/functions/build/Resolve-PSMDBuildStepParameter.ps1 @@ -0,0 +1,69 @@ +function Resolve-PSMDBuildStepParameter { +<# + .SYNOPSIS + Update missing build action parameters from the configuration system. + + .DESCRIPTION + Update missing build action parameters from the configuration system. + This command is for use within the defined code of build actions. + + .PARAMETER Parameters + The hashtable containing the currently specified parameters from the step configuration within the build project file. + Only settings not already defined there are taken from configuration. + + .PARAMETER FromArtifacts + The hashtable mapping parameters from artifacts. + This allows dynamically assigning artifacts to parameters. + + .PARAMETER ProjectName + The name of the project being executed. + Supplementary parameters taken from configuration will pick up settings based on this name: + "PSModuleDevelopment.BuildParam...*" + + .PARAMETER StepName + The name of the step being executed. + Supplementary parameters taken from configuration will pick up settings based on this name: + "PSModuleDevelopment.BuildParam...*" + + .EXAMPLE + PS C:\> Resolve-PSMDBuildStepParameter -Parameters $actualParameters -ProjectName VMDeployment -StepName 'Create Session' + + Adds parameters provided through configuration. +#> + [OutputType([hashtable])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [hashtable] + $Parameters, + + [Parameter(Mandatory = $true)] + [hashtable] + $FromArtifacts, + + [Parameter(Mandatory = $true)] + [string] + $ProjectName, + + [Parameter(Mandatory = $true)] + [string] + $StepName + ) + + process { + # Process parameters from Configuration + $configObject = Select-PSFConfig -FullName "PSModuleDevelopment.BuildParam.$ProjectName.$StepName.*" + foreach ($property in $configObject.PSObject.Properties) { + if ($property.Name -in '_Name', '_FullName', '_Depth', '_Children') { continue } + if ($Parameters.ContainsKey($property.Name)) { continue } + $Parameters[$property.Name] = $property.Value + } + + # Process parameters from Artifacts + foreach ($pair in $FromArtifacts.GetEnumerator()) { + $Parameters[$pair.Key] = (Get-PSMDBuildArtifact -Name $pair.Value).Value + } + + $Parameters + } +} \ No newline at end of file diff --git a/PSModuleDevelopment/functions/build/Select-PSMDBuildProject.ps1 b/PSModuleDevelopment/functions/build/Select-PSMDBuildProject.ps1 new file mode 100644 index 0000000..b915741 --- /dev/null +++ b/PSModuleDevelopment/functions/build/Select-PSMDBuildProject.ps1 @@ -0,0 +1,40 @@ +function Select-PSMDBuildProject +{ +<# + .SYNOPSIS + Set the specified build project as the default project. + + .DESCRIPTION + Set the specified build project as the default project. + This will have most other commands in this Component automatically use the specified project. + + .PARAMETER Path + Path to the project file to pick. + + .PARAMETER Register + Persist the choice as default build project file across PowerShell sessions. + + .EXAMPLE + PS C:\> Select-PSMDBuildProject -Path 'c:\code\Project\Project.build.json' + + Sets the specified build project as the default project. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Path, + + [switch] + $Register + ) + + process + { + Invoke-PSFProtectedCommand -ActionString 'Select-PSMDBuildProject.Testing' -ActionStringValues $Path -ScriptBlock { + $null = Get-PSMDBuildProject -Path $Path -ErrorAction Stop + } -Target $Path -EnableException $true -PSCmdlet $PSCmdlet + Set-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' -Value $Path + if ($Register) { Register-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' } + } +} diff --git a/PSModuleDevelopment/functions/build/Set-PSMDBuildStep.ps1 b/PSModuleDevelopment/functions/build/Set-PSMDBuildStep.ps1 new file mode 100644 index 0000000..07b3722 --- /dev/null +++ b/PSModuleDevelopment/functions/build/Set-PSMDBuildStep.ps1 @@ -0,0 +1,120 @@ +function Set-PSMDBuildStep { +<# + .SYNOPSIS + Create or update a step from a build project. + + .DESCRIPTION + Create or update a step from a build project. + + .PARAMETER Name + The name of the step. + All step names must be unique within a single build project. + + .PARAMETER Weight + The weight of the step. + Weight determines processing order, the lower the number the earlier it is executed. + + .PARAMETER Action + The name of the action to execute. + Use Get-PSMDBuildAction to get a list of available actions. + + .PARAMETER Parameters + The parameters this action should take. + See the action object to see a description of parameters, including which must be provided and which can be skipped. + + .PARAMETER Condition + A PSFramework filter condition that must apply for this action to be executed successfully. + Example Conditions: + Elevated + PS7Plus -and OSWindows + More Details: https://psframework.org/documentation/documents/psframework/filters.html + + .PARAMETER ConditionSet + The name of the condition set to use. + This is part of the PSFramework filter system: + https://psframework.org/documentation/documents/psframework/filters.html + + Specify as " " format. + Default Value: PSFramework Environment + + .PARAMETER Dependency + Any other steps that must successfully finished in order for this step to execute. + ALL of the listed steps must have succeeded, skipped steps do not count. + + .PARAMETER BuildProject + The build project file to work against. + Specify the full path to the build project file. + This parameter can be skipped if a default project file has been defined. + + .EXAMPLE + PS C:\> Set-PSMDBuildStep -Name 'Create Session' -Action new-pssession -Parameters @{ VMName = 'labdc1'; CredentialPath = "%ProjectRoot%\creds\labdc1.cred"; } + + Defines a new step named 'Create Session' using the 'new-pssession'-action. +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [int] + $Weight, + + [PsfArgumentCompleter('PSModuleDevelopment.Build.Action')] + [string] + $Action, + + [hashtable] + $Parameters, + + [string] + $Condition, + + [string] + $ConditionSet, + + [string[]] + $Dependency, + + [string] + $BuildProject + ) + + begin { + $projectPath = $BuildProject + if (-not $projectPath) { $projectPath = Get-PSFConfigValue -FullName 'PSModuleDevelopment.Build.Project.Selected' } + if (-not $projectPath) { throw "No Project path specified and none selected!" } + if (-not (Test-Path -Path $projectPath)) { + throw "Project file not found: $projectPath" + } + } + process { + $projectObject = Get-PSMDBuildProject -Path $projectPath | ConvertTo-PSFHashtable + $stepObject = $projectObject.Steps | Where-Object Name -EQ $Name | ConvertTo-PSFHashtable + if (-not $stepObject) { + $stepObject = [pscustomobject]@{ + PSTypeName = 'PSModuleDevelopment.Build.Step' + Name = $Name + Weight = 50 + Action = '' + Parameters = @{ } + Condition = '' + ConditionSet = 'PSFramework Environment' + Dependency = @() + } + } + if (Test-PSFParameterBinding -ParameterName Weight) { $stepObject.Weight = $Weight } + if (Test-PSFParameterBinding -ParameterName Action) { $stepObject.Action = $Action } + if (Test-PSFParameterBinding -ParameterName Parameters) { $stepObject.Parameters = $Parameters } + if (Test-PSFParameterBinding -ParameterName Condition) { $stepObject.Condition = $Condition } + if (Test-PSFParameterBinding -ParameterName ConditionSet) { $stepObject.ConditionSet = $ConditionSet } + if (Test-PSFParameterBinding -ParameterName Dependency) { $stepObject.Dependency = $Dependency } + + if (-not $stepObject.Action) { + throw "Failed to save Build Step $Name : No Action defined!" + } + $projectObject.Steps = @($projectObject.Steps | Where-Object Name -ne $Name) + @($stepObject) + $projectObject | Export-PsmdBuildProjectFile -OutPath $projectPath -ErrorAction Stop + } +} \ No newline at end of file diff --git a/PSModuleDevelopment/functions/help/Get-PSMDHelp.ps1 b/PSModuleDevelopment/functions/help/Get-PSMDHelp.ps1 index 98b7a6f..fa6bc5d 100644 --- a/PSModuleDevelopment/functions/help/Get-PSMDHelp.ps1 +++ b/PSModuleDevelopment/functions/help/Get-PSMDHelp.ps1 @@ -113,12 +113,8 @@ PS C:\> Get-PSMDHelp Get-Help "en-us" -Detailed Gets the detailed help text of Get-Help in English - - .NOTES - Version 1.0.0.0 - Author: Friedrich Weinmann - Created on: August 15th, 2016 #> + [Alias('hex')] [CmdletBinding(DefaultParameterSetName = "AllUsersView")] Param ( [Parameter(ParameterSetName = "Parameters", Mandatory = $true)] @@ -209,5 +205,4 @@ try { $steppablePipeline.End() } catch { throw } } -} -New-Alias -Name hex -Value Get-PSMDHelp -Scope Global -Option AllScope \ No newline at end of file +} \ No newline at end of file diff --git a/PSModuleDevelopment/functions/moduledebug/Import-PSMDModuleDebug.ps1 b/PSModuleDevelopment/functions/moduledebug/Import-PSMDModuleDebug.ps1 index 7eca92d..2800849 100644 --- a/PSModuleDevelopment/functions/moduledebug/Import-PSMDModuleDebug.ps1 +++ b/PSModuleDevelopment/functions/moduledebug/Import-PSMDModuleDebug.ps1 @@ -15,6 +15,7 @@ Imports the cPSNetwork module as it was configured to be imported using Set-ModuleDebug. #> + [Alias('ipmod')] [CmdletBinding()] param ( [string] @@ -40,5 +41,4 @@ [System.Management.Automation.ScriptBlock]::Create($____module.PostImportAction).Invoke() } } -} -New-Alias -Name ipmod -Value Import-ModuleDebug -Option AllScope -Scope Global \ No newline at end of file +} \ No newline at end of file diff --git a/PSModuleDevelopment/functions/moduledebug/Set-PSMDModuleDebug.ps1 b/PSModuleDevelopment/functions/moduledebug/Set-PSMDModuleDebug.ps1 index a9a8e65..92ea656 100644 --- a/PSModuleDevelopment/functions/moduledebug/Set-PSMDModuleDebug.ps1 +++ b/PSModuleDevelopment/functions/moduledebug/Set-PSMDModuleDebug.ps1 @@ -65,6 +65,7 @@ Note: Using Write-Host is generally - but not always - bad practice Note: Verbose output during module import is generally discouraged (doesn't apply to tests of course) #> + [Alias('smd')] [CmdletBinding(DefaultParameterSetName = "Name", SupportsShouldProcess = $true)] Param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Name", ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] @@ -176,5 +177,4 @@ } #endregion Name } -} -Set-Alias -Name smd -Value Set-PSMDModuleDebug -Option AllScope -Scope Global \ No newline at end of file +} \ No newline at end of file diff --git a/PSModuleDevelopment/functions/refactor/Read-PSMDScript.ps1 b/PSModuleDevelopment/functions/refactor/Read-PSMDScript.ps1 index 88a7105..b7f9fde 100644 --- a/PSModuleDevelopment/functions/refactor/Read-PSMDScript.ps1 +++ b/PSModuleDevelopment/functions/refactor/Read-PSMDScript.ps1 @@ -24,6 +24,7 @@ Parses all script files in the current directory #> + [Alias('parse')] [CmdletBinding()] param ( [Parameter(Position = 0, ParameterSetName = 'Script', Mandatory = $true)] @@ -78,5 +79,4 @@ } } } -} -Set-Alias -Name parse -Value Read-PSMDScript \ No newline at end of file +} \ No newline at end of file diff --git a/PSModuleDevelopment/functions/templating/Get-PSMDTemplate.ps1 b/PSModuleDevelopment/functions/templating/Get-PSMDTemplate.ps1 index 54e6168..00df036 100644 --- a/PSModuleDevelopment/functions/templating/Get-PSMDTemplate.ps1 +++ b/PSModuleDevelopment/functions/templating/Get-PSMDTemplate.ps1 @@ -84,8 +84,6 @@ begin { - Write-PSFMessage -Level InternalComment -Message "Bound parameters: $($PSBoundParameters.Keys -join ", ")" -Tag 'debug', 'start', 'param' - $prospects = @() } process @@ -97,7 +95,7 @@ foreach ($info in $templateInfos) { - $data = Import-Clixml $info.FullName + $data = Import-PSFClixml $info.FullName $data.Path = $info.FullName -replace '\.Info\.xml$','.xml' $prospects += $data } @@ -117,7 +115,7 @@ foreach ($info in $templateInfos) { - $data = Import-Clixml $info.FullName + $data = Import-PSFClixml $info.FullName $data.Path = $info.FullName -replace '-Info\.xml$', '.xml' $data.Store = $item.Name $prospects += $data diff --git a/PSModuleDevelopment/functions/templating/Invoke-PSMDTemplate.ps1 b/PSModuleDevelopment/functions/templating/Invoke-PSMDTemplate.ps1 index fea81e3..ce29e0c 100644 --- a/PSModuleDevelopment/functions/templating/Invoke-PSMDTemplate.ps1 +++ b/PSModuleDevelopment/functions/templating/Invoke-PSMDTemplate.ps1 @@ -1,5 +1,4 @@ -function Invoke-PSMDTemplate -{ +function Invoke-PSMDTemplate { <# .SYNOPSIS Creates a project/file from a template. @@ -79,6 +78,7 @@ #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] + [Alias('imt')] [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'NameStore')] @@ -99,6 +99,7 @@ $Path, [Parameter(Position = 2)] + [PSFramework.Validation.PsfValidateScript('PSFramework.Validate.FSPath.Folder', ErrorString = 'PSFramework.Validate.FSPath.Folder')] [string] $OutPath = (Get-PSFConfigValue -FullName 'PSModuleDevelopment.Template.OutPath' -Fallback "."), @@ -128,52 +129,31 @@ $EnableException ) - begin - { - #region Validate output path - try - { - $resolvedPath = Resolve-Path $OutPath -ErrorAction Stop - if (($resolvedPath | Measure-Object).Count -ne 1) - { - throw "Cannot resolve $OutPath to a single folder" - } - if ($resolvedPath.Provider -notlike "*FileSystem") - { - throw "Path $OutPath was not recognized as a filesystem path" - } - } - catch - { - Stop-PSFFunction -Message "Could not resolve output path to a valid folder: $OutPath" -EnableException $EnableException -ErrorRecord $_ -Tag 'fail', 'path', 'validate' - return - } - #endregion Validate output path - + begin { $templates = @() - switch ($PSCmdlet.ParameterSetName) - { + switch ($PSCmdlet.ParameterSetName) { 'NameStore' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Store $Store } 'NamePath' { $templates = Get-PSMDTemplate -TemplateName $TemplateName -Path $Path } } + if ($TemplateName -and -not $templates) { + Stop-PSFFunction -String 'Invoke-PSMDTemplate.Template.NotFound' -StringValues $TemplateName -EnableException $EnableException -Cmdlet $PSCmdlet + return + } #region Parameter Processing if (-not $Parameters) { $Parameters = @{ } } if ($Name) { $Parameters["Name"] = $Name } - foreach ($config in (Get-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.*')) - { + foreach ($config in (Get-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.*')) { $cfgName = $config.Name -replace '^.+\.([^\.]+)$', '$1' - if (-not $Parameters.ContainsKey($cfgName)) - { + if (-not $Parameters.ContainsKey($cfgName)) { $Parameters[$cfgName] = $config.Value } } #endregion Parameter Processing #region Helper function - function Invoke-Template - { + function Invoke-Template { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( @@ -200,16 +180,13 @@ ) Write-PSFMessage -Level Verbose -Message "Processing template $($item)" -Tag 'template', 'invoke' -FunctionName Invoke-PSMDTemplate - $templateData = Import-Clixml -Path $Template.Path -ErrorAction Stop + $templateData = Import-PSFClixml -Path $Template.Path -ErrorAction Stop #region Process Parameters - foreach ($parameter in $templateData.Parameters) - { + foreach ($parameter in $templateData.Parameters) { if (-not $parameter) { continue } - if (-not $Parameters.ContainsKey($parameter)) - { + if (-not $Parameters.ContainsKey($parameter)) { if ($Silent) { throw "Parameter not specified: $parameter" } - try - { + try { $value = Read-Host -Prompt "Enter value for parameter '$parameter'" -ErrorAction Stop $Parameters[$parameter] = $value } @@ -221,17 +198,13 @@ #region Scripts $scriptParameters = @{ } - if (-not $Raw) - { - foreach ($scriptParam in $templateData.Scripts.Values) - { + if (-not $Raw) { + foreach ($scriptParam in $templateData.Scripts.Values) { if (-not $scriptParam) { continue } try { $scriptParameters[$scriptParam.Name] = "$([scriptblock]::Create($scriptParam.StringScript).Invoke())" } - catch - { + catch { if ($Silent) { throw (New-Object System.Exception("Scriptblock $($scriptParam.Name) failed during execution: $_", $_.Exception)) } - else - { + else { Write-PSFMessage -Level Warning -Message "Scriptblock $($scriptParam.Name) failed during execution. Please specify a custom value or use CTRL+C to terminate creation" -ErrorRecord $_ -FunctionName "Invoke-PSMDTemplate" -ModuleName 'PSModuleDevelopment' $scriptParameters[$scriptParam.Name] = Read-Host -Prompt "Value for script $($scriptParam.Name)" } @@ -240,17 +213,14 @@ } #endregion Scripts - switch ($templateData.Type.ToString()) - { + switch ($templateData.Type.ToString()) { #region File "File" { - foreach ($child in $templateData.Children) - { + foreach ($child in $templateData.Children) { Write-TemplateItem -Item $child -Path $OutPath -Encoding $Encoding -ParameterFlat $Parameters -ParameterScript $scriptParameters -Raw $Raw } - if ($Raw -and $templateData.Scripts.Values) - { + if ($Raw -and $templateData.Scripts.Values) { $templateData.Scripts.Values | Export-Clixml -Path (Join-Path $OutPath "_PSMD_ParameterScripts.xml") } } @@ -260,34 +230,28 @@ "Project" { #region Resolve output folder - if (-not $NoFolder) - { - if ($Parameters["Name"]) - { + if (-not $NoFolder) { + if ($Parameters["Name"]) { $projectName = $Parameters["Name"] $projectFullName = Join-Path $OutPath $projectName - if ((Test-Path $projectFullName) -and (-not $Force)) - { + if ((Test-Path $projectFullName) -and (-not $Force)) { throw "Project root folder already exists: $projectFullName" } $newFolder = New-Item -Path $OutPath -Name $Parameters["Name"] -ItemType Directory -ErrorAction Stop -Force } - else - { + else { throw "Parameter Name is needed to create a project without setting the -NoFolder parameter!" } } else { $newFolder = Get-Item $OutPath } #endregion Resolve output folder - foreach ($child in $templateData.Children) - { + foreach ($child in $templateData.Children) { Write-TemplateItem -Item $child -Path $newFolder.FullName -Encoding $Encoding -ParameterFlat $Parameters -ParameterScript $scriptParameters -Raw $Raw } #region Write Config File (Raw) - if ($Raw) - { + if ($Raw) { $guid = [System.Guid]::NewGuid().ToString() $optionsTemplate = @" @{ @@ -299,11 +263,9 @@ þþþPLACEHOLDER-$($guid)þþþ } "@ - if ($params = $templateData.Scripts.Values) - { + if ($params = $templateData.Scripts.Values) { $list = @() - foreach ($param in $params) - { + foreach ($param in $params) { $list += @" $($param.Name) = { $($param.StringScript) @@ -312,9 +274,8 @@ } $optionsTemplate = $optionsTemplate -replace "þþþPLACEHOLDER-$($guid)þþþ", ($list -join "`n`n") } - else - { - $optionsTemplate = $optionsTemplate -replace "þþþPLACEHOLDER-$($guid)þþþ","" + else { + $optionsTemplate = $optionsTemplate -replace "þþþPLACEHOLDER-$($guid)þþþ", "" } $configFile = Join-Path $newFolder.FullName "PSMDTemplate.ps1" @@ -326,8 +287,7 @@ } } - function Write-TemplateItem - { + function Write-TemplateItem { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( @@ -350,46 +310,37 @@ $Raw ) - Write-PSFMessage -Level Verbose -Message "Creating file: $($Item.Name) ($($Item.RelativePath))" -FunctionName Invoke-PSMDTemplate -ModuleName PSModuleDevelopment -Tag 'create','template' + Write-PSFMessage -Level Verbose -Message "Creating file: $($Item.Name) ($($Item.RelativePath))" -FunctionName Invoke-PSMDTemplate -ModuleName PSModuleDevelopment -Tag 'create', 'template' $identifier = $Item.Identifier $isFile = $Item.GetType().Name -eq 'TemplateItemFile' #region File - if ($isFile) - { + if ($isFile) { $fileName = $Item.Name - if (-not $Raw) - { - foreach ($param in $Item.FileSystemParameterFlat) - { - $fileName = $fileName -replace "$($identifier)$([regex]::Escape($param))$($identifier)",$ParameterFlat[$param] + if (-not $Raw) { + foreach ($param in $Item.FileSystemParameterFlat) { + $fileName = $fileName.Replace("$($identifier)$($param)$($identifier)", $ParameterFlat[$param], $true, $null) } - foreach ($param in $Item.FileSystemParameterScript) - { - $fileName = $fileName -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param] + foreach ($param in $Item.FileSystemParameterScript) { + $fileName = $fileName.Replace("$($identifier)$($param)$($identifier)", $ParameterScript[$param], $true, $null) } } $destPath = Join-Path $Path $fileName - if ($Item.PlainText) - { + if ($Item.PlainText) { $text = $Item.Value - if (-not $Raw) - { - foreach ($param in $Item.ContentParameterFlat) - { - $text = $text -replace "$($identifier)$([regex]::Escape($param))$($identifier)", $ParameterFlat[$param] + if (-not $Raw) { + foreach ($param in $Item.ContentParameterFlat) { + $text = $text.Replace("$($identifier)$($param)$($identifier)", $ParameterFlat[$param], $true, $null) } - foreach ($param in $Item.ContentParameterScript) - { - $text = $text -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param] + foreach ($param in $Item.ContentParameterScript) { + $text = $text.Replace("$($identifier)!$($param)!$($identifier)", $ParameterScript[$param], $true, $null) } } [System.IO.File]::WriteAllText($destPath, $text, $Encoding) } - else - { + else { $bytes = [System.Convert]::FromBase64String($Item.Value) [System.IO.File]::WriteAllBytes($destPath, $bytes) } @@ -397,24 +348,19 @@ #endregion File #region Folder - else - { + else { $folderName = $Item.Name - if (-not $Raw) - { - foreach ($param in $Item.FileSystemParameterFlat) - { + if (-not $Raw) { + foreach ($param in $Item.FileSystemParameterFlat) { $folderName = $folderName -replace "$($identifier)$([regex]::Escape($param))$($identifier)", $ParameterFlat[$param] } - foreach ($param in $Item.FileSystemParameterScript) - { + foreach ($param in $Item.FileSystemParameterScript) { $folderName = $folderName -replace "$($identifier)!$([regex]::Escape($param))!$($identifier)", $ParameterScript[$param] } } $folder = New-Item -Path $Path -Name $folderName -ItemType Directory - foreach ($child in $Item.Children) - { + foreach ($child in $Item.Children) { Write-TemplateItem -Item $child -Path $folder.FullName -Encoding $Encoding -ParameterFlat $ParameterFlat -ParameterScript $ParameterScript -Raw $Raw } } @@ -422,27 +368,27 @@ } #endregion Helper function } - process - { + process { if (Test-PSFFunctionInterrupt) { return } - foreach ($item in $Template) - { - if ($PSCmdlet.ShouldProcess($item, "Invoking template")) - { - try { Invoke-Template -Template $item -OutPath $resolvedPath.ProviderPath -NoFolder $NoFolder -Encoding $Encoding -Parameters $Parameters.Clone() -Raw $Raw -Silent $Silent } - catch { Stop-PSFFunction -Message "Failed to invoke template $($item)" -EnableException $EnableException -ErrorRecord $_ -Target $item -Tag 'fail', 'template', 'invoke' -Continue } - } + $invokeParam = @{ + Parameters = $Parameters.Clone() + OutPath = Resolve-PSFPath -Path $OutPath + NoFolder = $NoFolder + Encoding = $Encoding + Raw = $Raw + Silent = $Silent } - foreach ($item in $templates) - { - if ($PSCmdlet.ShouldProcess($item, "Invoking template")) - { - try { Invoke-Template -Template $item -OutPath $resolvedPath.ProviderPath -NoFolder $NoFolder -Encoding $Encoding -Parameters $Parameters.Clone() -Raw $Raw -Silent $Silent } - catch { Stop-PSFFunction -Message "Failed to invoke template $($item)" -EnableException $EnableException -ErrorRecord $_ -Target $item -Tag 'fail', 'template', 'invoke' -Continue } - } + + foreach ($item in $Template) { + Invoke-PSFProtectedCommand -ActionString 'Invoke-PSMDTemplate.Invoking' -ActionStringValues $item -Target $item -ScriptBlock { + Invoke-Template @invokeParam -Template $item + } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue + } + foreach ($item in $templates) { + Invoke-PSFProtectedCommand -ActionString 'Invoke-PSMDTemplate.Invoking' -ActionStringValues $item -Target $item -ScriptBlock { + Invoke-Template @invokeParam -Template $item + } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } } -} - -if (-not (Test-Path Alias:\imt)) { Set-Alias -Name imt -Value Invoke-PSMDTemplate } \ No newline at end of file +} \ No newline at end of file diff --git a/PSModuleDevelopment/functions/templating/New-PSMDDotNetProject.ps1 b/PSModuleDevelopment/functions/templating/New-PSMDDotNetProject.ps1 index 8e2fdd8..a34ca7c 100644 --- a/PSModuleDevelopment/functions/templating/New-PSMDDotNetProject.ps1 +++ b/PSModuleDevelopment/functions/templating/New-PSMDDotNetProject.ps1 @@ -59,6 +59,7 @@ - It will skip the automatic restore of the project on create #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] + [Alias('dotnetnew')] [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Create')] Param ( [Parameter(Position = 0, Mandatory = $true, ParameterSetName = 'Create')] @@ -175,6 +176,4 @@ & dotnet.exe new $dotNetArgs } } -} - -New-Alias -Name dotnetnew -Value New-PSMDDotNetProject -Option AllScope -Scope Global -ErrorAction Ignore \ No newline at end of file +} \ No newline at end of file diff --git a/PSModuleDevelopment/functions/utility/Find-PSMDFileContent.ps1 b/PSModuleDevelopment/functions/utility/Find-PSMDFileContent.ps1 index bf78b92..94094b0 100644 --- a/PSModuleDevelopment/functions/utility/Find-PSMDFileContent.ps1 +++ b/PSModuleDevelopment/functions/utility/Find-PSMDFileContent.ps1 @@ -31,6 +31,7 @@ Searches all module files for the string 'Get-Test'. #> + [Alias('find')] [CmdletBinding()] Param ( [Parameter(Mandatory = $true, Position = 0)] @@ -61,5 +62,4 @@ Get-ChildItem -Path $Path -Recurse | Where-Object Extension -Match $Extension | Select-String -Pattern $Pattern } -} -New-Alias -Name find -Value Find-PSMDFileContent -Scope Global -Option AllScope \ No newline at end of file +} \ No newline at end of file diff --git a/PSModuleDevelopment/functions/utility/Restart-PSMDShell.ps1 b/PSModuleDevelopment/functions/utility/Restart-PSMDShell.ps1 index 0e34124..67a8a5a 100644 --- a/PSModuleDevelopment/functions/utility/Restart-PSMDShell.ps1 +++ b/PSModuleDevelopment/functions/utility/Restart-PSMDShell.ps1 @@ -39,6 +39,7 @@ Author: Friedrich Weinmann Created on: August 6th, 2016 #> + [Alias('rss', 'Restart-Shell')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] Param ( [Switch] @@ -72,6 +73,4 @@ if (-not $NoExit) { exit } } } -} -New-Alias -Name Restart-Shell -Value Restart-PSMDShell -Option AllScope -Scope Global -New-Alias -Name rss -Value Restart-PSMDShell -Option AllScope -Scope Global \ No newline at end of file +} \ No newline at end of file diff --git a/PSModuleDevelopment/internal/buildActions/command.action.ps1 b/PSModuleDevelopment/internal/buildActions/command.action.ps1 new file mode 100644 index 0000000..9a8e034 --- /dev/null +++ b/PSModuleDevelopment/internal/buildActions/command.action.ps1 @@ -0,0 +1,74 @@ +$action = { + param ( + $Parameters + ) + + $rootPath = $Parameters.RootPath + $actualParameters = $Parameters.Parameters + $actualParameters = Resolve-PSMDBuildStepParameter -Parameters $actualParameters -FromArtifacts $Parameters.ParametersFromArtifacts -ProjectName $Parameters.ProjectName -StepName $Parameters.StepName + + #region Process Parameters + if (-not $actualParameters.Command) { + throw "Mandatory parameter: Command not specified" + } + + if ($actualParameters.Command -is [System.Management.Automation.ScriptBlock]) { + $scriptblock = $actualParameters.Command + } + else { + try { $scriptblock = [scriptblock]::Create($actualParameters.Command) } + catch { + throw "Error parsing command '$($actualParameters.Command)' : $_" + } + } + + $actualArguments = foreach ($argument in $actualParameters.ArgumentList) { + if ($argument -isnot [string]) { + $argument + continue + } + if ($argument -notlike '%!*!%') { + $argument + continue + } + $artifactName = $argument -replace '^%!(.+)!%$', '$1' + $artifactObject = Get-PSMDBuildArtifact -Name $artifactName + if (-not $artifactObject) { throw "Artifact for arguments not found: $artifactName" } + $artifactObject.Value + } + + $inSession = $null + if ($actualParameters.InSession) { + if ($actualParameters.InSession -is [System.Management.Automation.Runspaces.PSSession]) { + $inSession = $actualParameters.InSession + } + $artifactObject = Get-PSMDBuildArtifact -Name $actualParameters.InSession + if (-not $artifactObject) { throw "Artifact for parameter InSession not found: $($actualParameters.InSession)" } + if ($artifactObject.Value -isnot [System.Management.Automation.Runspaces.PSSession]) { throw "Artifact for parameter InSession ($($actualParameters.InSession)) is not a pssession!" } + $inSession = $artifactObject.Value + } + #endregion Process Parameters + + #region Execution + $invokeParam = @{ + ScriptBlock = $scriptblock + ArgumentList = $actualArguments + } + if ($inSession) { $invokeParam.Session = $inSession } + try { Invoke-Command @invokeParam -ErrorAction Stop } + catch { throw } + #endregion Execution +} + +$params = @{ + Name = 'command' + Action = $action + Description = 'Execute a scriptblock' + Parameters = @{ + Command = '(mandatory) Scriptcode to run' + ArgumentList = 'Any number of arguments to pass to the command. To insert artifacts, specify a string with the special notation "%!ArtifactName!%"' + InSession = 'Execute the scriptfile in the target PSSession. Either provide a full session object or an artifact name pointing at one.' + } +} + +Register-PSMDBuildAction @params \ No newline at end of file diff --git a/PSModuleDevelopment/internal/buildActions/copy-item.action.ps1 b/PSModuleDevelopment/internal/buildActions/copy-item.action.ps1 new file mode 100644 index 0000000..bb8aa67 --- /dev/null +++ b/PSModuleDevelopment/internal/buildActions/copy-item.action.ps1 @@ -0,0 +1,73 @@ +$action = { + param ( + $Parameters + ) + + $rootPath = $Parameters.RootPath + $actualParameters = $Parameters.Parameters + $actualParameters = Resolve-PSMDBuildStepParameter -Parameters $actualParameters -FromArtifacts $Parameters.ParametersFromArtifacts -ProjectName $Parameters.ProjectName -StepName $Parameters.StepName + + #region Utility Functions + function ConvertTo-PSSession { + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + $InputObject + ) + process { + if ($InputObject -is [System.Management.Automation.Runspaces.PSSession]) { + return $InputObject + } + $artifactValue = (Get-PSMDBuildArtifact -Name $InputObject).Value + if ($artifactValue -is [System.Management.Automation.Runspaces.PSSession]) { + return $artifactValue + } + } + } + #endregion Utility Functions + + if (-not ($actualParameters.Path -and $actualParameters.Destination)) { + throw "Invalid parameters! Specify both Path and Destination." + } + + $paths = $actualParameters.Path -replace '%ProjectRoot%', $rootPath + $copyParam = @{ + Destination = $actualParameters.Destination -replace '%ProjectRoot%', $rootPath + } + if ($actualParameters.Recurse) { $copyParam.Recurse = $true } + if ($actualParameters.Force) { $copyParam.Force = $true } + if ($actualParameters.FromSession) { + $fromSession = $actualParameters.FromSession | ConvertTo-PSSession + if (-not $fromSession) { + throw "FromSession $($actualParameters.FromSession) not found!" + } + $copyParam.FromSession = $fromSession + } + if ($actualParameters.ToSession) { + $toSession = $actualParameters.ToSession | ConvertTo-PSSession + if (-not $toSession) { + throw "ToSession $($actualParameters.ToSession) not found!" + } + $copyParam.ToSession = $toSession + } + foreach ($path in $paths) { + try { Copy-Item @copyParam -Path $path -ErrorAction Stop } + catch { throw } + } +} + +$params = @{ + Name = 'copy-item' + Action = $action + Description = 'Copies files & folders from A to B' + Parameters = @{ + Path = '(mandatory) Path(s) to copy. Use "%ProjectRoot%" to reference to the root path containing the build file.' + Destination = '(mandatory) Path to copy to. Use "%ProjectRoot%" to reference to the root path containing the build file.' + FromSession = 'Artifact Name of the PSSession to copy from.' + ToSession = 'Artifact Name of the PSSession to copy to.' + Recurse = 'Whether to copy child items' + Force = 'Whether to use force (Remove destination items)' + } +} + +Register-PSMDBuildAction @params \ No newline at end of file diff --git a/PSModuleDevelopment/internal/buildActions/new-pssession.action.ps1 b/PSModuleDevelopment/internal/buildActions/new-pssession.action.ps1 new file mode 100644 index 0000000..6406f4e --- /dev/null +++ b/PSModuleDevelopment/internal/buildActions/new-pssession.action.ps1 @@ -0,0 +1,44 @@ +$action = { + param ( + $Parameters + ) + + $rootPath = $Parameters.RootPath + $actualParameters = $Parameters.Parameters + $actualParameters = Resolve-PSMDBuildStepParameter -Parameters $actualParameters -FromArtifacts $Parameters.ParametersFromArtifacts -ProjectName $Parameters.ProjectName -StepName $Parameters.StepName + + if (-not $actualParameters.ArtifactName) { throw "No ArtifactName specified! Unable to publish remoting session for build." } + if (-not ($actualParameters.VMName -or $actualParameters.ComputerName)) { throw "Neither ComputerName nor VMName specified, unable to connect to nothing!" } + if ($actualParameters.VMName -and $actualParameters.ComputerName) { throw "Both ComputerName and VMName specified, unable to connect to both at once!" } + + $credential = $null + if ($actualParameters.CredentialPath) { + $path = $actualParameters.CredentialPath -replace '%ProjectRoot%', $rootPath + try { $credential = Import-PSFClixml -Path $path -ErrorAction Stop } + catch { throw "Error accessing credentials from $path : $_" } + } + + $paramNewPSSession = @{ } + if ($actualParameters.VMName) { $paramNewPSSession.VMName = $actualParameters.VMName } + if ($actualParameters.ComputerName) { $paramNewPSSession.ComputerName = $actualParameters.ComputerName } + if ($credential) { $paramNewPSSession.Credential = $credential } + + try { $session = New-PSSession @paramNewPSSession -ErrorAction Stop } + catch { throw "Error establishing PS Remoting session: $_" } + + Publish-PSMDBuildArtifact -Name $actualParameters.ArtifactName -Value $session -Tag pssession +} + +$params = @{ + Name = 'new-pssession' + Action = $action + Description = 'Establish a PSSession to a target computer and provide it as an artifact' + Parameters = @{ + ComputerName = 'The Computer to connect to' + VMName = 'The virtual machine to which to connect to via the HyperV VM Bus' + CredentialPath = 'The path to the credentials to use for the connection. Use %ProjectRoot% to insert the folder path to where the buildfile is located' + ArtifactName = '(mandatory) The name under which to publish the session as an artifact' + } +} + +Register-PSMDBuildAction @params \ No newline at end of file diff --git a/PSModuleDevelopment/internal/buildActions/remove-item.action.ps1 b/PSModuleDevelopment/internal/buildActions/remove-item.action.ps1 new file mode 100644 index 0000000..624ac1b --- /dev/null +++ b/PSModuleDevelopment/internal/buildActions/remove-item.action.ps1 @@ -0,0 +1,64 @@ +$action = { + param ( + $Parameters + ) + + $rootPath = $Parameters.RootPath + $actualParameters = $Parameters.Parameters + $actualParameters = Resolve-PSMDBuildStepParameter -Parameters $actualParameters -FromArtifacts $Parameters.ParametersFromArtifacts -ProjectName $Parameters.ProjectName -StepName $Parameters.StepName + + if (-not $actualParameters.Path) { + throw "Invalid parameters! Specify a Path to delete." + } + + $paths = $actualParameters.Path -replace '%ProjectRoot%', $rootPath + $deleteParam = @{ } + if ($actualParameters.Recurse) { $deleteParam.Recurse = $true } + if ($actualParameters.Force) { $deleteParam.Force = $true } + + $inSession = $null + if ($actualParameters.InSession) { + if ($actualParameters.InSession -is [System.Management.Automation.Runspaces.PSSession]) { + $inSession = $actualParameters.InSession + } + $artifactObject = Get-PSMDBuildArtifact -Name $actualParameters.InSession + if (-not $artifactObject) { throw "Artifact for parameter InSession not found: $($actualParameters.InSession)" } + if ($artifactObject.Value -isnot [System.Management.Automation.Runspaces.PSSession]) { throw "Artifact for parameter InSession ($($actualParameters.InSession)) is not a pssession!" } + $inSession = $artifactObject.Value + } + + if ($inSession) { + $failed = Invoke-Command -Session $inSession -ScriptBlock { + param ($DeleteParam, $Paths) + + foreach ($path in $Paths) { + if (-not (Get-Item -Path $path -Force -ErrorAction Ignore)) { continue } + try { Remove-Item @DeleteParam -Path $path -ErrorAction Stop } + catch { return $_ } + } + } -ArgumentList $deleteParam, $paths + if ($failed) { + throw $failed + } + } + + foreach ($path in $paths) { + if (-not (Get-Item -Path $path -Force -ErrorAction Ignore)) { continue } + try { Remove-Item @DeleteParam -Path $path -ErrorAction Stop } + catch { throw } + } +} + +$params = @{ + Name = 'remove-item' + Action = $action + Description = 'Removes files or folders' + Parameters = @{ + Path = '(mandatory) Path(s) to the item(s) to delete. Use "%ProjectRoot%" to reference to the root path containing the build file.' + InSession = 'Artifact Name of the PSSession within which to execute the deletion' + Recurse = 'Whether to delete child items' + Force = 'Whether to use force' + } +} + +Register-PSMDBuildAction @params \ No newline at end of file diff --git a/PSModuleDevelopment/internal/buildActions/remove-pssession.action.ps1 b/PSModuleDevelopment/internal/buildActions/remove-pssession.action.ps1 new file mode 100644 index 0000000..a6e80e5 --- /dev/null +++ b/PSModuleDevelopment/internal/buildActions/remove-pssession.action.ps1 @@ -0,0 +1,48 @@ +$action = { + param ( + $Parameters + ) + + $rootPath = $Parameters.RootPath + $actualParameters = $Parameters.Parameters + $actualParameters = Resolve-PSMDBuildStepParameter -Parameters $actualParameters -FromArtifacts $Parameters.ParametersFromArtifacts -ProjectName $Parameters.ProjectName -StepName $Parameters.StepName + + if ($actualParameters.All) { + foreach ($artifact in Get-PSMDBuildArtifact -Tag pssession) { + try { + $artifact.Value | Remove-PSSession -ErrorAction Stop + Remove-PSMDBuildArtifact -Name $artifact.Name + } + catch { + throw "Failed to remove PSSession artifact $($artifact.Name) to $($artifact.Value) | $_" + } + } + } + elseif ($actualParameters.ArtifactName) { + $artifact = Get-PSMDBuildArtifact -Name $actualParameters.ArtifactName + if ($artifact) { + try { + $artifact.Value | Remove-PSSession -ErrorAction Stop + Remove-PSMDBuildArtifact -Name $artifact.Name + } + catch { + throw "Failed to remove PSSession artifact $($artifact.Name) to $($artifact.Value) | $_" + } + } + } + else { + throw "Invalid parameters! Specify either 'All' or 'ArtifactName' in step definition." + } +} + +$params = @{ + Name = 'remove-pssession' + Action = $action + Description = 'Removes a PSSession that was previously established with the new-pssession action' + Parameters = @{ + ArtifactName = 'The name under which to publish the session as an artifact' + All = 'Whether all PSSession artifacts should be removed' + } +} + +Register-PSMDBuildAction @params \ No newline at end of file diff --git a/PSModuleDevelopment/internal/buildActions/script.action.ps1 b/PSModuleDevelopment/internal/buildActions/script.action.ps1 new file mode 100644 index 0000000..6826213 --- /dev/null +++ b/PSModuleDevelopment/internal/buildActions/script.action.ps1 @@ -0,0 +1,70 @@ +$action = { + param ( + $Parameters + ) + + $rootPath = $Parameters.RootPath + $actualParameters = $Parameters.Parameters + $actualParameters = Resolve-PSMDBuildStepParameter -Parameters $actualParameters -FromArtifacts $Parameters.ParametersFromArtifacts -ProjectName $Parameters.ProjectName -StepName $Parameters.StepName + + #region Process Parameters + if (-not $actualParameters.Path) { + throw "Mandatory parameter: Path not specified" + } + + $scriptPath = $actualParameters.Path -replace '%ProjectRoot%', $rootPath + + if (-not (Test-Path $scriptPath)) { + throw "Cannot find resolved script path: $scriptPath" + } + + $actualArguments = foreach ($argument in $actualParameters.ArgumentList) { + if ($argument -isnot [string]) { + $argument + continue + } + if ($argument -notlike '%!*!%') { + $argument + continue + } + $artifactName = $argument -replace '^%!(.+)!%$', '$1' + $artifactObject = Get-PSMDBuildArtifact -Name $artifactName + if (-not $artifactObject) { throw "Artifact for arguments not found: $artifactName" } + $artifactObject.Value + } + + $inSession = $null + if ($actualParameters.InSession) { + if ($actualParameters.InSession -is [System.Management.Automation.Runspaces.PSSession]) { + $inSession = $actualParameters.InSession + } + $artifactObject = Get-PSMDBuildArtifact -Name $actualParameters.InSession + if (-not $artifactObject) { throw "Artifact for parameter InSession not found: $($actualParameters.InSession)" } + if ($artifactObject.Value -isnot [System.Management.Automation.Runspaces.PSSession]) { throw "Artifact for parameter InSession ($($actualParameters.InSession)) is not a pssession!" } + $inSession = $artifactObject.Value + } + #endregion Process Parameters + + #region Execution + $invokeParam = @{ + FilePath = $scriptPath + ArgumentList = $actualArguments + } + if ($inSession) { $invokeParam.Session = $inSession } + try { Invoke-Command @invokeParam -ErrorAction Stop } + catch { throw } + #endregion Execution +} + +$params = @{ + Name = 'script' + Action = $action + Description = 'Execute a scriptfile' + Parameters = @{ + Path = '(mandatory) Path to the scriptfile to run. Use %ProjectRoot% to reference the same folder the build action file is stored in.' + ArgumentList = 'Any number of arguments to pass to the scripts. To insert artifacts, specify a string with the special notation "%!ArtifactName!%"' + InSession = 'Execute the scriptfile in the target PSSession. Either provide a full session object or an artifact name pointing at one.' + } +} + +Register-PSMDBuildAction @params \ No newline at end of file diff --git a/PSModuleDevelopment/internal/configurations/build.ps1 b/PSModuleDevelopment/internal/configurations/build.ps1 new file mode 100644 index 0000000..291bf24 --- /dev/null +++ b/PSModuleDevelopment/internal/configurations/build.ps1 @@ -0,0 +1 @@ +Set-PSFConfig -Module PSModuleDevelopment -Name 'Build.Project.Selected' -Value '' -Validation string -Initialize -Description 'Path of the selected build project. Used when running Invoke-PSMDBuildProject without specifying a build file.' \ No newline at end of file diff --git a/PSModuleDevelopment/internal/configurations/debug.ps1 b/PSModuleDevelopment/internal/configurations/debug.ps1 index 51e68c0..d6999b3 100644 --- a/PSModuleDevelopment/internal/configurations/debug.ps1 +++ b/PSModuleDevelopment/internal/configurations/debug.ps1 @@ -1 +1 @@ -Set-PSFConfig -Module PSModuleDevelopment -Name 'Debug.ConfigPath' -Value "$($path_FileUserShared)\InfernalAssociates\PowerShell\PSModuleDevelopment\config.xml" -Initialize -Validation string -Description 'The path to where the module debugging information is being stored. Used in the *-PSMDModuleDebug commands.' \ No newline at end of file +Set-PSFConfig -Module PSModuleDevelopment -Name 'Debug.ConfigPath' -Value (Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath "InfernalAssociates/PowerShell/PSModuleDevelopment/config.xml") -Initialize -Validation string -Description 'The path to where the module debugging information is being stored. Used in the *-PSMDModuleDebug commands.' \ No newline at end of file diff --git a/PSModuleDevelopment/internal/configurations/template.ps1 b/PSModuleDevelopment/internal/configurations/template.ps1 index f1065ad..79a6981 100644 --- a/PSModuleDevelopment/internal/configurations/template.ps1 +++ b/PSModuleDevelopment/internal/configurations/template.ps1 @@ -9,7 +9,7 @@ Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.ParameterDefault.Com Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.BinaryExtensions' -Value @('.dll', '.exe', '.pdf', '.doc', '.docx', '.xls', '.xlsx') -Initialize -Description "When creating a template, files with these extensions will be included as raw bytes and not interpreted for parameter insertion." # Define the default store. To add more stores, just add a similar setting with a different last name segment -Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.Store.Default' -Value "$path_FileUserShared/WindowsPowerShell/PSModuleDevelopment/Templates" -Initialize -Validation "string" -Description "Path to the default directory where PSModuleDevelopment will store its templates. You can add additional stores by creating the same setting again, only changing the last name segment to a new name and configuring a separate path." +Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.Store.Default' -Value (Join-Path -Path (Get-PSFPath -Name AppData) -ChildPath "WindowsPowerShell/PSModuleDevelopment/Templates") -Initialize -Validation "string" -Description "Path to the default directory where PSModuleDevelopment will store its templates. You can add additional stores by creating the same setting again, only changing the last name segment to a new name and configuring a separate path." Set-PSFConfig -Module 'PSModuleDevelopment' -Name 'Template.Store.PSModuleDevelopment' -Value "$script:ModuleRoot/internal/templates" -Initialize -Validation "string" -Description "Path to the templates shipped in PSModuleDevelopment" # Define the default path to create from templates in diff --git a/PSModuleDevelopment/internal/configurations/utility.ps1 b/PSModuleDevelopment/internal/configurations/utility.ps1 index 6bf4866..bddf4d2 100644 --- a/PSModuleDevelopment/internal/configurations/utility.ps1 +++ b/PSModuleDevelopment/internal/configurations/utility.ps1 @@ -1,5 +1,5 @@ Set-PSFConfig -Module PSModuleDevelopment -Name 'Module.Path' -Value "" -Initialize -Validation "string" -Handler { } -Description "The path to the module currently under development. Used as default path by commnds that work within a module directory." -Set-PSFConfig -Module PSModuleDevelopment -Name 'Package.Path' -Value "$env:TEMP" -Initialize -Validation "string" -Description "The default output path when exporting a module into a nuget package." +Set-PSFConfig -Module PSModuleDevelopment -Name 'Package.Path' -Value (Get-PSFPath -Name Temp) -Initialize -Validation "string" -Description "The default output path when exporting a module into a nuget package." Set-PSFConfig -Module PSModuleDevelopment -Name 'Find.DefaultExtensions' -Value '^\.ps1$|^\.psd1$|^\.psm1$|^\.cs$' -Initialize -Validation string -Description 'The pattern to use to select files to scan when using Find-PSMDFileContent.' Set-PSFConfig -Module PSModuleDevelopment -Name "ShowSyntax.ParmsNotFound" -Value "Red" -Initialize -Validation "string" -Handler { } -Description "The color to be used for the parameters that could not be found." diff --git a/PSModuleDevelopment/internal/functions/build/Export-PsmdBuildProjectFile.ps1 b/PSModuleDevelopment/internal/functions/build/Export-PsmdBuildProjectFile.ps1 new file mode 100644 index 0000000..1759ba6 --- /dev/null +++ b/PSModuleDevelopment/internal/functions/build/Export-PsmdBuildProjectFile.ps1 @@ -0,0 +1,48 @@ +function Export-PsmdBuildProjectFile { +<# + .SYNOPSIS + Exports a build project object to file. + + .DESCRIPTION + Exports a build project object to file. + Strips out all superfluous properties on steps to improve readability of output. + + .PARAMETER OutPath + The path to write the file to. + + .PARAMETER ProjectObject + The build project to export. + + .EXAMPLE + PS C:\> $projectObject | Export-PsmdBuildProjectFile -OutPath $outPath + + Exports the specified build project object to file. +#> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $OutPath, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + $ProjectObject + ) + + process { + $steps = foreach ($step in $ProjectObject.Steps) { + $newStep = $step | ConvertTo-PSFHashtable -Include Name, Weight, Action + if ($step.Dependency) { $newStep.Dependency = $step.Dependency } + if ($step.Parameters) { + $parameters = $step.Parameters | ConvertTo-PSFHashtable + if ($parameters.Count -gt 0) { $newStep.Parameters = $parameters } + } + if ($step.Condition -and $step.ConditionSet) { + $newStep.Condition = $step.Condition + $newStep.ConditionSet = $step.ConditionSet + } + [PSCustomObject]$newStep + } + $ProjectObject.Steps = $steps | Sort-Object Weight + $ProjectObject | ConvertTo-Json -Depth 10 | Set-Content -Path $OutPath -Encoding UTF8 -ErrorAction Stop + } +} \ No newline at end of file diff --git a/PSModuleDevelopment/internal/scripts/postimport.ps1 b/PSModuleDevelopment/internal/scripts/postimport.ps1 index 5b93318..51a1137 100644 --- a/PSModuleDevelopment/internal/scripts/postimport.ps1 +++ b/PSModuleDevelopment/internal/scripts/postimport.ps1 @@ -21,6 +21,11 @@ foreach ($file in (Get-ChildItem "$($script:ModuleRoot)\internal\maintenance\*.p . Import-ModuleFile -Path $file.FullName } +# Load Build Actions +foreach ($file in (Get-ChildItem "$($script:ModuleRoot)\internal\buildActions\*.ps1" -ErrorAction Ignore)) { + . Import-ModuleFile -Path $file.FullName +} + # Load License . Import-ModuleFile -Path "$($script:ModuleRoot)\internal\scripts\license.ps1" diff --git a/PSModuleDevelopment/internal/scripts/variables.ps1 b/PSModuleDevelopment/internal/scripts/variables.ps1 index ef13dc1..30d090d 100644 --- a/PSModuleDevelopment/internal/scripts/variables.ps1 +++ b/PSModuleDevelopment/internal/scripts/variables.ps1 @@ -11,4 +11,8 @@ else # Defaults to $Env:AppData on Windows $path_FileUserShared = Join-Path $Env:AppData "$psVersionName\PSFramework\Config" if (-not $Env:AppData) { $path_FileUserShared = Join-Path ([Environment]::GetFolderPath("ApplicationData")) "$psVersionName\PSFramework\Config" } -} \ No newline at end of file +} + +# Store of registered build actions +$script:buildActions = @{ } +$script:buildArtifacts = @{ } diff --git a/PSModuleDevelopment/internal/tepp/build.tepp.ps1 b/PSModuleDevelopment/internal/tepp/build.tepp.ps1 new file mode 100644 index 0000000..e430665 --- /dev/null +++ b/PSModuleDevelopment/internal/tepp/build.tepp.ps1 @@ -0,0 +1,3 @@ +Register-PSFTeppScriptblock -Name 'PSModuleDevelopment.Build.Action' -ScriptBlock { + (Get-PSMDBuildAction).Name +} \ No newline at end of file diff --git a/PSModuleDevelopment/xml/PSModuleDevelopment.Format.ps1xml b/PSModuleDevelopment/xml/PSModuleDevelopment.Format.ps1xml index 5351685..19a6410 100644 --- a/PSModuleDevelopment/xml/PSModuleDevelopment.Format.ps1xml +++ b/PSModuleDevelopment/xml/PSModuleDevelopment.Format.ps1xml @@ -1,6 +1,45 @@  + + + PSModuleDevelopment.Build.StepResult + + PSModuleDevelopment.Build.StepResult + + + + + + + + + + + + + + + Count + + + Action + + + Status + + + Step + + + Data + + + + + + + PSModuleDevelopment.File.Command diff --git a/build/filesAfter.txt b/build/filesAfter.txt index fbf452a..92c18a9 100644 --- a/build/filesAfter.txt +++ b/build/filesAfter.txt @@ -5,5 +5,6 @@ internal\scriptblocks\*.ps1 internal\tepp\*.tepp.ps1 internal\tepp\assignment.ps1 internal\maintenance\*.ps1 +internal\buildActions\*.ps1 internal\scripts\license.ps1 internal\scripts\moduledebug.ps1 \ No newline at end of file diff --git a/library/PSModuleDevelopment/PSModuleDevelopment/PSModuleDevelopment.csproj b/library/PSModuleDevelopment/PSModuleDevelopment/PSModuleDevelopment.csproj index 25f15a9..2673b30 100644 --- a/library/PSModuleDevelopment/PSModuleDevelopment/PSModuleDevelopment.csproj +++ b/library/PSModuleDevelopment/PSModuleDevelopment/PSModuleDevelopment.csproj @@ -31,6 +31,10 @@ 4 + + ..\..\..\..\psframework\PSFramework\bin\PSFramework.dll + False + @@ -54,7 +58,11 @@ + + + + diff --git a/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ParameterBase.cs b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ParameterBase.cs new file mode 100644 index 0000000..451f9ce --- /dev/null +++ b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ParameterBase.cs @@ -0,0 +1,27 @@ +using System; + +namespace PSModuleDevelopment.Template.Parameter +{ + /// + /// Base class for all kinds of parameters gen 2+ + /// + [Serializable] + public abstract class ParameterBase + { + /// + /// Name of the parameter + /// + public string Name; + + /// + /// Description of the parameter + /// + public string Description; + + /// + /// Get the value associated with this parameter + /// + /// The value to insert into the artifact generated from the template + public abstract string GetValue(); + } +} diff --git a/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ParameterPrompt.cs b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ParameterPrompt.cs new file mode 100644 index 0000000..c96803e --- /dev/null +++ b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ParameterPrompt.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace PSModuleDevelopment.Template.Parameter +{ + /// + /// A template parameter where the user is prompted for input. + /// + [Serializable] + public class ParameterPrompt : ParameterBase + { + /// + /// The value provided by the user + /// + public string Value; + + /// + /// List of legal values to provide + /// + public List ValidateSet = new List(); + + /// + /// A validation pattern that needs to be met. + /// + public string ValidatePattern; + + /// + /// An error description that will be shown if the user provides invalid input to a parameter with pattern validation. + /// + public string PatternError; + + /// + /// Test whether the input meets the validation rules + /// + /// The value to test + /// Whether the value is valid. + public bool TestValue(string Value) + { + if (ValidateSet.Count > 0 && !ValidateSet.Contains(Value, StringComparer.InvariantCultureIgnoreCase)) + return false; + if (!String.IsNullOrEmpty(ValidatePattern) && !Regex.IsMatch(Value, ValidatePattern)) + return false; + + return true; + } + + /// + /// Return the value specified by the user. + /// + /// The value specified by the user + public override string GetValue() + { + return Value; + } + } +} diff --git a/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ParameterScript.cs b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ParameterScript.cs new file mode 100644 index 0000000..c757440 --- /dev/null +++ b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ParameterScript.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +using PSFramework.Utility; + +namespace PSModuleDevelopment.Template.Parameter +{ + /// + /// Parameter type executing + /// + [Serializable] + public class ParameterScript : ParameterBase + { + /// + /// The scriptblock to execute. + /// Wrapped as string for serialization purposes. + /// + public string ScriptBlock + { + get + { + if (_ScriptBlock == null) + return ""; + return _ScriptBlock.ToString(); + } + set + { + _ScriptBlock = new PsfScriptBlock(System.Management.Automation.ScriptBlock.Create(value)); + } + } + private PsfScriptBlock _ScriptBlock; + + /// + /// The value of the scriptblock. + /// Populated by the GetValue() method usually called with the "StartUp" timing. + /// + public string Value; + + /// + /// When exactly during the template process should this scriptblock be executed? + /// + public ScriptExecutionTime Timing = ScriptExecutionTime.StartUp; + + /// + /// Setting this to true will cause the Invoke-PSMDTemplate command to omit inserting values for the + /// + public bool SkipInsert; + + /// + /// Returns the string value of the scriptblock by executing it! + /// + /// The string value of the scriptblock by executing it! + public override string GetValue() + { + if (String.IsNullOrEmpty(Value)) + try { Value = (string)LanguagePrimitives.ConvertTo(_ScriptBlock.InvokeEx(true, true, false), typeof(string)); } + catch (Exception e) { Value = $""; } + return Value; + } + + /// + /// Execute the scriptblock "Just-in-time" during either PreItemCreation or PostItemCreation Timing. + /// + /// The file/directory info object of the object recently or about to be created + /// Returns a string value resulting from the scriptblock to insert + public string GetInTimeValue(FileSystemInfo Info) + { + try { return (string)LanguagePrimitives.ConvertTo(_ScriptBlock.InvokeEx(Info, true, true, false), typeof(string)); } + catch (Exception e) { return $""; } + } + + /// + /// Execute the scriptblock "Just-in-time" during either PreItemCreation or PostItemCreation Timing. + /// + /// The path to the file/directory info object of the object recently or about to be created + /// Whether the object (about to be) created is a file. + /// Returns a string value resulting from the scriptblock to insert + public string GetInTimeValue(string Path, bool IsFile) + { + if (IsFile) + return GetInTimeValue(new FileInfo(Path)); + return GetInTimeValue(new DirectoryInfo(Path)); + } + } +} diff --git a/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ScriptExecutionTime.cs b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ScriptExecutionTime.cs new file mode 100644 index 0000000..e8a797e --- /dev/null +++ b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Parameter/ScriptExecutionTime.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PSModuleDevelopment.Template.Parameter +{ + /// + /// When will a specific scriptblock parameter be executed? + /// + public enum ScriptExecutionTime + { + /// + /// Executed when starting the overall template invocation + /// + StartUp = 1, + + /// + /// Executed before an individual item using it is created. + /// Values will be inserted into the file-content before writing to disk if applicable. + /// + PreItemCreation = 2, + + /// + /// Executed after the individual item using it has been created. + /// Output will be discarded, but scriptblock will receive path of file / folder. + /// + PostItemCreation = 3, + + /// + /// Executed after the entire project has been written. + /// Enables post-processing. + /// + Conclusion = 4 + } +} diff --git a/library/PSModuleDevelopment/PSModuleDevelopment/Template/Template.cs b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Template.cs index 77b7ff8..0fa0d85 100644 --- a/library/PSModuleDevelopment/PSModuleDevelopment/Template/Template.cs +++ b/library/PSModuleDevelopment/PSModuleDevelopment/Template/Template.cs @@ -58,26 +58,41 @@ public class Template /// public Dictionary Scripts = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// + /// List of generation 2 parameters to include + /// + public Dictionary Parameters2 = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// /// Items in the root directory of the template (which may contain children themselves). /// public List Children = new List(); + /// + /// What design generation is the template? + /// + public int Generation = 1; + /// /// Returns the template digest used as index file. /// /// A TemplateInfo object describing this template. public TemplateInfo ToTemplateInfo() { + List parameters = new List(Parameters); + if (Parameters2.Count > 0) + parameters.AddRange(Parameters2.Values.Where(o => o.GetType().Name == "ParameterPrompt").Select(o => o.Name)); + TemplateInfo info = new TemplateInfo(); info.Author = Author; info.CreatedOn = CreatedOn; info.Description = Description; info.Name = Name; - info.Parameters = Parameters; + info.Parameters = parameters; info.Tags = Tags; info.Type = Type; info.Version = Version; + info.Generation = Generation; return info; } diff --git a/library/PSModuleDevelopment/PSModuleDevelopment/Template/TemplateInfo.cs b/library/PSModuleDevelopment/PSModuleDevelopment/Template/TemplateInfo.cs index 586bc40..26f956e 100644 --- a/library/PSModuleDevelopment/PSModuleDevelopment/Template/TemplateInfo.cs +++ b/library/PSModuleDevelopment/PSModuleDevelopment/Template/TemplateInfo.cs @@ -62,6 +62,11 @@ public class TemplateInfo /// public string Path; + /// + /// What template generation is this file? + /// + public int Generation = 1; + /// /// The version-qualified name of the template /// diff --git a/templates/AzureFunction/PSMDTemplate.ps1 b/templates/AzureFunction/PSMDTemplate.ps1 index b77849a..131be22 100644 --- a/templates/AzureFunction/PSMDTemplate.ps1 +++ b/templates/AzureFunction/PSMDTemplate.ps1 @@ -1,10 +1,11 @@ @{ - TemplateName = 'AzureFunction' - Version = "1.0.0.0" - AutoIncrementVersion = $true - Tags = 'azure', 'function' - Author = 'Friedrich Weinmann' - Description = 'Basic Azure Function Template' - Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template - Scripts = @{ } + TemplateName = 'AzureFunction' + Version = "2.0.0" + AutoIncrementVersion = $true + Tags = 'azure', 'function' + Author = 'Friedrich Weinmann' + Description = 'Basic Azure Function Template' + Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template + Scripts = @{ } + NoFolder = $true # Whether invoking this template should generate a new folder ... or not. } \ No newline at end of file diff --git a/templates/AzureFunction/build/build.ps1 b/templates/AzureFunction/build/build.ps1 new file mode 100644 index 0000000..9a79a68 --- /dev/null +++ b/templates/AzureFunction/build/build.ps1 @@ -0,0 +1,23 @@ +param ( + [string] + $Repository = 'PSGallery' +) +$workingDirectory = Split-Path $PSScriptRoot + +# Prepare output path and copy function folder +Remove-Item -Path "$workingDirectory/publish" -Recurse -Force -ErrorAction Ignore +$buildFolder = New-Item -Path $workingDirectory -Name 'publish' -ItemType Directory -Force -ErrorAction Stop +Copy-Item -Path "$workingDirectory/function/*" -Destination $buildFolder.FullName -Recurse -Force + +# Process Dependencies +$requiredModules = (Import-PowerShellDataFile -Path "$workingDirectory/þnameþ/þnameþ.psd1").RequiredModules +foreach ($module in $requiredModules) { + Save-Module -Name $module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository +} + +# Process Function Module +Copy-Item -Path "$workingDirectory/þnameþ" -Destination "$($buildFolder.FullName)/modules" -Force -Recurse + +# Package & Cleanup +Compress-Archive -Path "$($buildFolder.FullName)/*" -DestinationPath "$workingDirectory/Function.zip" +Remove-Item -Path $buildFolder.FullName -Recurse -Force -ErrorAction Ignore \ No newline at end of file diff --git a/templates/AzureFunction/host.json b/templates/AzureFunction/function/host.json similarity index 100% rename from templates/AzureFunction/host.json rename to templates/AzureFunction/function/host.json diff --git a/templates/AzureFunction/modules/readme.md b/templates/AzureFunction/function/modules/readme.md similarity index 100% rename from templates/AzureFunction/modules/readme.md rename to templates/AzureFunction/function/modules/readme.md diff --git a/templates/AzureFunction/function/profile.ps1 b/templates/AzureFunction/function/profile.ps1 new file mode 100644 index 0000000..be04a7d --- /dev/null +++ b/templates/AzureFunction/function/profile.ps1 @@ -0,0 +1,132 @@ +# Azure Functions profile.ps1 +# +# This profile.ps1 will get executed every "cold start" of your Function App. +# "cold start" occurs when: +# +# * A Function App starts up for the very first time +# * A Function App starts up after being de-allocated due to inactivity +# +# You can define helper functions, run commands, or specify environment variables +# NOTE: any variables defined that are not environment variables will get reset after the first execution +# Authenticate with Azure PowerShell using MSI. +# Remove this if you are not planning on using MSI or Azure PowerShell. + +if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) +{ + Connect-AzAccount -Identity +} + +# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. +# Enable-AzureRmAlias +# You can also define functions or aliases that can be referenced in any of your PowerShell functions. + +function Write-FunctionResult { + <# + .SYNOPSIS + Reports back the output / result of the function app. + + .DESCRIPTION + Reports back the output / result of the function app. + + .PARAMETER Status + Whether the function succeeded or not. + + .PARAMETER Body + Any data to include in the response. + + .EXAMPLE + PS C:\> Write-FunctionResult -Status OK -Body $newUser + + Reports success while returning the content of $newUser as output + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.Net.HttpStatusCode] + $Status, + + [AllowNull()] + $Body + ) + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $Status + Body = $Body + }) +} + +function Get-RestParameterValue { + <# + .SYNOPSIS + Extract the exact value of a parameter provided by the user. + + .DESCRIPTION + Extract the exact value of a parameter provided by the user. + Expects either query or body parameters from the rest call to the http trigger. + + .PARAMETER Request + The request object provided as part of the function call. + + .PARAMETER Name + The name of the parameter to provide. + + .EXAMPLE + PS C:\> Get-RestParameterValue -Request $Request -Name Type + + Returns the value of the parameter "Type", as provided by the caller + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + $Request, + + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + if ($Request.Query.$Name) { + return $Request.Query.$Name + } + $Request.Body.$Name +} + +function Get-RestParameter { + <# + .SYNOPSIS + Parses the rest request parameters for all values matching parameters on the specified command. + + .DESCRIPTION + Parses the rest request parameters for all values matching parameters on the specified command. + Returns a hashtable ready for splatting. + Does NOT assert mandatory parameters are specified, so command invocation may fail. + + .PARAMETER Request + The original rest request object, containing the caller's information such as parameters. + + .PARAMETER Command + The command to which to bind input parameters. + + .EXAMPLE + PS C:\> Get-RestParameter -Request $Request -Command Get-AzUser + + Retrieves all parameters on the incoming request that match a parameter on Get-AzUser + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true)] + $Request, + + [Parameter(Mandatory = $true)] + [string] + $Command + ) + + $commandInfo = Get-Command -Name $Command + $results = @{ } + foreach ($parameter in $commandInfo.Parameters.Keys) { + $value = Get-RestParameterValue -Request $Request -Name $parameter + if ($null -ne $value) { $results[$parameter] = $value } + } + $results +} \ No newline at end of file diff --git a/templates/AzureFunction/requirements.psd1 b/templates/AzureFunction/function/requirements.psd1 similarity index 100% rename from templates/AzureFunction/requirements.psd1 rename to templates/AzureFunction/function/requirements.psd1 diff --git a/templates/AzureFunction/profile.ps1 b/templates/AzureFunction/profile.ps1 deleted file mode 100644 index 3d09b77..0000000 --- a/templates/AzureFunction/profile.ps1 +++ /dev/null @@ -1,21 +0,0 @@ -# Azure Functions profile.ps1 -# -# This profile.ps1 will get executed every "cold start" of your Function App. -# "cold start" occurs when: -# -# * A Function App starts up for the very first time -# * A Function App starts up after being de-allocated due to inactivity -# -# You can define helper functions, run commands, or specify environment variables -# NOTE: any variables defined that are not environment variables will get reset after the first execution -# Authenticate with Azure PowerShell using MSI. -# Remove this if you are not planning on using MSI or Azure PowerShell. - -if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) -{ - Connect-AzAccount -Identity -} - -# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. -# Enable-AzureRmAlias -# You can also define functions or aliases that can be referenced in any of your PowerShell functions. \ No newline at end of file diff --git "a/templates/AzureFunction/\303\276name\303\276/functions/readme.md" "b/templates/AzureFunction/\303\276name\303\276/functions/readme.md" new file mode 100644 index 0000000..81c52a2 --- /dev/null +++ "b/templates/AzureFunction/\303\276name\303\276/functions/readme.md" @@ -0,0 +1,3 @@ +# Functions + +Place all your function code here diff --git "a/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psd1" "b/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psd1" new file mode 100644 index 0000000..9c8f039 --- /dev/null +++ "b/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psd1" @@ -0,0 +1,103 @@ +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'þnameþ.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = 'þ{ New-Guid }þ' + + # Author of this module + Author = 'þauthorþ' + + # Company or vendor of this module + CompanyName = 'þcompanyþ' + + # Copyright statement for this module + Copyright = '(c) þauthorþ. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'þdescriptionþ' + + # Minimum version of the PowerShell engine required by this module + # PowerShellVersion = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @('bin\my.dll') + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions 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 functions to export. + 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 = '*' + + # Variables to export from this module + # 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 = '*' + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # 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 = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable +} \ No newline at end of file diff --git "a/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psm1" "b/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psm1" new file mode 100644 index 0000000..2509b6f --- /dev/null +++ "b/templates/AzureFunction/\303\276name\303\276/\303\276name\303\276.psm1" @@ -0,0 +1,3 @@ +foreach ($file in Get-ChildItem $PSScriptRoot\functions -Recurse -Filter '*.ps1') { + . $file.FullName +} \ No newline at end of file diff --git a/templates/AzureFunctionRest/run.ps1 b/templates/AzureFunctionRest/run.ps1 index 1f5b0ea..6c6cc83 100644 --- a/templates/AzureFunctionRest/run.ps1 +++ b/templates/AzureFunctionRest/run.ps1 @@ -1,33 +1,18 @@ -using namespace System.Net - -# Input bindings are passed in via param block. -param ($Request, +param ( + $Request, - $TriggerMetadata) + $TriggerMetadata +) -# Write to the Azure Functions log stream. -Write-Host "PowerShell HTTP trigger function processed a request." -# Interact with query parameters or the body of the request. -$name = $Request.Query.Name -if (-not $name) -{ - $name = $Request.Body.Name -} +Write-Host "Trigger: þnameþ has been invoked" -if ($name) -{ - $status = [HttpStatusCode]::OK - $body = "Hello $name" -} -else -{ - $status = [HttpStatusCode]::BadRequest - $body = "Please pass a name on the query string or in the request body." -} +$parameters = Get-RestParameter -Request $Request -Command þnameþ -# Associate values to output bindings by calling 'Push-OutputBinding'. -Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $status - Body = $body - }) +try { $results = þnameþ @parameters -ErrorAction Stop } +catch { + Write-FunctionResult -Status InternalServerError -Body $_ + $_ | Out-Host + return +} +Write-FunctionResult -Status OK -Body $results \ No newline at end of file diff --git a/templates/PSFProject/.github/FUNDING.yml b/templates/PSFProject/.github/FUNDING.yml new file mode 100644 index 0000000..4b16f59 --- /dev/null +++ b/templates/PSFProject/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/templates/PSFProject/.github/workflows/build.yml b/templates/PSFProject/.github/workflows/build.yml new file mode 100644 index 0000000..2bd1de8 --- /dev/null +++ b/templates/PSFProject/.github/workflows/build.yml @@ -0,0 +1,23 @@ +on: + push: + branches: + - master + +jobs: + build: + + runs-on: windows-2019 + + steps: + - uses: actions/checkout@v1 + - name: Install Prerequisites + run: .\build\vsts-prerequisites.ps1 + shell: powershell + - name: Validate + run: .\build\vsts-validate.ps1 + shell: powershell + - name: Build + run: .\build\vsts-build.ps1 -ApiKey $env:APIKEY + shell: powershell + env: + APIKEY: ${{ secrets.ApiKey }} diff --git a/templates/PSFProject/.github/workflows/validate.yml b/templates/PSFProject/.github/workflows/validate.yml new file mode 100644 index 0000000..64387f3 --- /dev/null +++ b/templates/PSFProject/.github/workflows/validate.yml @@ -0,0 +1,15 @@ +on: [pull_request] + +jobs: + validate: + + runs-on: windows-2019 + + steps: + - uses: actions/checkout@v1 + - name: Install Prerequisites + run: .\build\vsts-prerequisites.ps1 + shell: powershell + - name: Validate + run: .\build\vsts-validate.ps1 + shell: powershell diff --git a/templates/PSFProject/PSMDTemplate.ps1 b/templates/PSFProject/PSMDTemplate.ps1 index 05aa08e..37887a9 100644 --- a/templates/PSFProject/PSMDTemplate.ps1 +++ b/templates/PSFProject/PSMDTemplate.ps1 @@ -1,6 +1,6 @@ @{ TemplateName = 'PSFProject' - Version = "1.3.2.0" + Version = "1.3.3" AutoIncrementVersion = $true Tags = 'module','psframework' Author = 'Friedrich Weinmann' @@ -38,4 +38,5 @@ $null = New-Item -Path "$PSScriptRoot\..\.." -Name TestResults -ItemType Directo '$config.TestResult.Enabled = $true' } } + NoFolder = $true } \ No newline at end of file diff --git a/templates/PSFTests/PSMDTemplate.ps1 b/templates/PSFTests/PSMDTemplate.ps1 index 770fb51..7396c33 100644 --- a/templates/PSFTests/PSMDTemplate.ps1 +++ b/templates/PSFTests/PSMDTemplate.ps1 @@ -1,6 +1,6 @@ @{ TemplateName = 'PSFTests' # Insert name of template - Version = "2.0.0.0" # Version to build to + Version = "2.0.1" # Version to build to AutoIncrementVersion = $true # If a newer version than specified is present, instead of the specified version, make it one greater than the existing template Tags = @('Tests', 'PSFramework') # Insert Tags as desired Author = 'Friedrich Weinmann' # The author of the template, not the file / project created from it diff --git a/templates/PSFTests/general/PSScriptAnalyzer.Tests.ps1 b/templates/PSFTests/general/PSScriptAnalyzer.Tests.ps1 index 74e5a65..a99b60d 100644 --- a/templates/PSFTests/general/PSScriptAnalyzer.Tests.ps1 +++ b/templates/PSFTests/general/PSScriptAnalyzer.Tests.ps1 @@ -12,7 +12,9 @@ if ($SkipTest) { return } $global:__pester_data.ScriptAnalyzer = New-Object System.Collections.ArrayList Describe 'Invoking PSScriptAnalyzer against commandbase' { - $commandFiles = Get-ChildItem -Path $CommandPath -Recurse | Where-Object Name -like "*.ps1" + $commandFiles = foreach ($path in $CommandPath) { + Get-ChildItem -Path $path -Recurse | Where-Object Name -like "*.ps1" + } $scriptAnalyzerRules = Get-ScriptAnalyzerRule foreach ($file in $commandFiles) diff --git a/templates/PSFTests/general/strings.Exceptions.ps1 b/templates/PSFTests/general/strings.Exceptions.ps1 index 0a11c98..b4c91f2 100644 --- a/templates/PSFTests/general/strings.Exceptions.ps1 +++ b/templates/PSFTests/general/strings.Exceptions.ps1 @@ -15,5 +15,22 @@ $exceptions['LegalSurplus'] = @( $exceptions['LegalSurplus'] = @( ) +<# +A list of entries that MAY be used without needing to have text defined. +This is intended for modules (re-)using strings provided by another module +#> +$exceptions['NoTextNeeded'] = @( + 'Validate.FSPath' + 'Validate.FSPath.File' + 'Validate.FSPath.FileOrParent' + 'Validate.FSPath.Folder' + 'Validate.Path' + 'Validate.Path.Container' + 'Validate.Path.Leaf' + 'Validate.TimeSpan.Positive' + 'Validate.Uri.Absolute' + 'Validate.Uri.Absolute.File' + 'Validate.Uri.Absolute.Https' +) $exceptions \ No newline at end of file diff --git a/templates/PSFTests/general/strings.Tests.ps1 b/templates/PSFTests/general/strings.Tests.ps1 index 5045dc1..eb83c05 100644 --- a/templates/PSFTests/general/strings.Tests.ps1 +++ b/templates/PSFTests/general/strings.Tests.ps1 @@ -9,17 +9,19 @@ Describe "Testing localization strings" { - $moduleRoot = (Get-Module þnameþ).ModuleBase + $moduleRoot = (Get-Module VHDX).ModuleBase $stringsResults = Export-PSMDString -ModuleRoot $moduleRoot $exceptions = & "$global:testroot\general\strings.Exceptions.ps1" foreach ($stringEntry in $stringsResults) { if ($stringEntry.String -eq "key") { continue } # Skipping the template default entry - It "Should be used & have text: $($stringEntry.String)" -TestCases @{ stringEntry = $stringEntry } { + It "Should be used & have text: $($stringEntry.String)" -TestCases @{ stringEntry = $stringEntry; exceptions = $exceptions } { if ($exceptions.LegalSurplus -notcontains $stringEntry.String) { $stringEntry.Surplus | Should -BeFalse - } - $stringEntry.Text | Should -Not -BeNullOrEmpty + } + if ($exceptions.NoTextNeeded -notcontains $stringEntry.String) { + $stringEntry.Text | Should -Not -BeNullOrEmpty + } } } } \ No newline at end of file