psmodulebuildhelper.psm1
#.ExternalHelp PSModuleBuildHelper-help.xml function Get-BuildEnvironment { <# .SYNOPSIS Get properties required to build the project. .DESCRIPTION Get the properties required to build the project, or elements of the project. All areas of the build process takes it's details from the data produced by this function. To influence part of the build process the data produced only need be altered. .EXAMPLE Get-BuildEnvironment Get build information for the current or any child directories. .OUTPUTS [PSObject] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release The idea came from the Indented.Build project (https://github.com/indented-automation/Indented.Build) and heavily modified. Credit should be given to that project. .LINK Get-ProjectEnvironment .LINK Get-TestEnvironment #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "", Justification = "Easiest way of jumping out of the block when one fails")] [OutputType([PSObject])] [CmdletBinding()] Param ( # The type of release we should be building. If this is 'Unknown' then # the release type will be deteremined from the commit message. [String] [ValidateSet( 'Major', 'Minor', 'Build', 'None', 'Unknown' )] $ReleaseType, # The GitHub Username that has write access to this repo. [string] $GitHubUsername = '', # The GitHub Api Key that has write access to this repo. [string] $GitHubApiKey = '', # The PowerShell Gallery key for publishing the module. [string] $PSGalleryApiKey = '', # Filename of the PowerShell ScriptAnalyzer settings file. This file # will be searched for under the project root. [string] $PSSASettingsName = 'PSScriptAnalyzerSettings.psd1', # The PowerShell ScriptAnalyzer Custom Rules folder. This folder will be # searched for under the project root and any .psd1 files found under # teh folder will be used. [string] $PSSACustomRulesFolderName = 'CustomAnalyzerRules' ) if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } $buildInfo = Get-ProjectEnvironment @( 'LatestVersion', 'ReleaseVersion', 'ReleaseType', 'PSGalleryApiKey', 'RepoBranch', 'RepoLastCommitHash', ` 'RepoLastCommitMessage', 'GitHubUsername', 'GitHubApiKey', 'SourceManifestPath', 'SourceModulePath', ` 'BuildPath', 'BuildManifestPath', 'BuildModulePath', 'PSSASettingsPath', 'PSSACustomRulesPath', ` 'BuildArtifactPath' ) | ForEach-Object { $buildInfo | Add-Member -MemberType NoteProperty -Name $_ -Value '' } # ReleaseType & PSGallery API Key if ([string]::IsNullOrEmpty($ReleaseType) -or $ReleaseType -eq 'Unknown') { Write-Verbose "Determining 'ReleaseType' from the last commit message." $buildInfo.ReleaseType = Get-ReleaseType -CommitMessage (Get-GitLastCommitMessage) } else { $buildInfo.ReleaseType = $ReleaseType } $buildInfo.PSGalleryApiKey = $PSGalleryApiKey # Git $buildInfo.GitHubUsername = $GitHubUsername $buildInfo.GitHubApiKey = $GitHubApiKey try { $buildInfo.RepoBranch = Get-GitBranchName -ErrorAction SilentlyContinue $buildInfo.RepoLastCommitHash = Get-GitLastCommitHash -ErrorAction SilentlyContinue $buildInfo.RepoLastCommitMessage = Get-GitLastCommitMessage -ErrorAction SilentlyContinue } catch { } # Source Paths $buildInfo.SourceManifestPath = Join-Path -Path $buildInfo.SourcePath -ChildPath "$($buildInfo.ModuleName).psd1" $buildInfo.SourceModulePath = Join-Path -Path $buildInfo.SourcePath -ChildPath "$($buildInfo.ModuleName).psm1" # Versions if (Test-Path -Path $buildInfo.SourceManifestPath) { $buildInfo.LatestVersion = Get-ManifestVersion -Path $buildInfo.SourceManifestPath $buildInfo.ReleaseVersion = Get-NextReleaseVersion -LatestVersion $buildInfo.LatestVersion -ReleaseType $buildInfo.ReleaseType } else { throw "Source manifest '$($buildInfo.SourceManifestPath)' does not exist." } # Build paths $buildPath = Join-Path -Path (Join-Path -Path $buildInfo.ProjectRootPath -ChildPath 'buildoutput') -ChildPath $buildInfo.ReleaseVersion $buildInfo.BuildPath = $buildPath $buildInfo.BuildManifestPath = Join-Path -Path $buildPath -ChildPath "$($buildInfo.ModuleName).psd1" $buildInfo.BuildModulePath = Join-Path -Path $buildPath -ChildPath "$($buildInfo.ModuleName).psm1" # Build Abstract $buildInfo.BuildArtifactPath = Join-Path -Path $buildInfo.ProjectRootPath -ChildPath "$($buildInfo.ModuleName)-$($buildInfo.ReleaseVersion).zip" # PSSA $settingsPath = Get-ChildItem -Path $PSSASettingsName -File -Recurse | Select-Object -First 1 if ($settingsPath) { $buildInfo.PSSASettingsPath = $settingsPath.FullName } else { Write-Verbose "Could not find PSScriptAnalyzer Settings file '$PSSASettingsName'." } $path = Get-ChildItem -Path $PSSACustomRulesFolderName -Directory -Recurse | Select-Object -First 1 if ($path -and (Test-Path -Path (Join-Path -Path $path.FullName -ChildPath '*.psd1'))) { $buildInfo.PSSACustomRulesPath = $path.FullName } else { Write-Verbose "No PSScriptAnalyzer Custom Rules folder '$PSSACustomRulesFolderName' found." } $buildInfo } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-BuildItem { <# .SYNOPSIS Gets the files to be used in the build. .DESCRIPTION Gets a list of files to be used in the build from the 'source' folder. .EXAMPLE Get-BuildItem -Type 'Static' -Path 'c:\mymodule\source' Gets a list of the static build items from the path 'c:\mymodule\source' .OUTPUTS [System.IO.FileInfo], [System.IO.DirectoryInfo] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release This function was lifted as is from Indented.Build (https://github.com/indented-automation/Indented.Build) so all credit goes to that project. .LINK #> [CmdletBinding()] [OutputType([System.IO.FileInfo], [System.IO.DirectoryInfo])] Param ( # Gets items by type. # # ShouldMerge - *.ps1 files from enum*, class*, priv*, pub* and # InitializeModule if present. # Static - Files which are not within a well known top-level # folder. Captures help content in en-US, format # files, configuration files, etc. [Parameter(Mandatory)] [ValidateSet('ShouldMerge', 'Static')] [String]$Type, # The path to the module 'source' folder. [Parameter(Mandatory, ValueFromPipeline)] [string]$Path, # Exclude script files containing PowerShell classes. [Switch]$ExcludeClass ) Push-Location $Path $itemTypes = [Ordered]@{ enumeration = 'enum*' class = 'class*' private = 'priv*' public = 'pub*' initialisation = 'InitializeModule.ps1' } if ($Type -eq 'ShouldMerge') { foreach ($itemType in $itemTypes.Keys) { if ($itemType -ne 'class' -or ($itemType -eq 'class' -and -not $ExcludeClass)) { $items = Get-ChildItem $itemTypes[$itemType] -Recurse -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer -and $_.Extension -eq '.ps1' -and $_.Length -gt 0 } $orderingFilePath = Join-Path $itemTypes[$itemType] 'order.txt' if (Test-Path $orderingFilePath) { [String[]]$order = Get-Content (Resolve-Path $orderingFilePath).Path $items = $items | Sort-Object { $index = $order.IndexOf($_.BaseName) if ($index -eq -1) { [Int32]::MaxValue } else { $index } }, Name } $items } } } elseif ($Type -eq 'Static') { [String[]]$exclude = $itemTypes.Values + '*.config', 'test*', 'doc', 'help', '.build*.ps1' # Should work, fails when testing. # Get-ChildItem -Exclude $exclude foreach ($item in Get-ChildItem) { $shouldExclude = $false foreach ($exclusion in $exclude) { if ($item.Name -like $exclusion) { $shouldExclude = $true } } if (-not $shouldExclude) { $item } } } Pop-Location } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-BuildOperatingSystemDetail { <# .SYNOPSIS Gets the operating system of the build system. .DESCRIPTION Gets the operating system of the build system in the following formt: - OSName - name of the operating system (Microsoft Windows 10 Pro) - OSArchitecture - x86 or x64 (64-bit) - Version - Version number (10.0.16299) The function is essentially a wrapper around Get-CimInstance win32_operatingsystem. .EXAMPLE Get-BuildOperatingSystem Returns the name, architecture and version of the current operating system. .OUTPUTS [PSObject] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-BuildPowerShellDetail .LINK Get-BuildEnvironment .LINK Get-BuildSystemEnvironment #> [CmdletBinding()] [OutputType([PSObject])] Param () Get-CimInstance -ClassName win32_operatingsystem -Property Caption, OSArchitecture, Version | ` Select-Object -Property @{l = 'OSName'; e = {$_.Caption} }, OSArchitecture, Version } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-BuildPowerShellDetail { <# .SYNOPSIS Return the $PSVersionTable as an object. .DESCRIPTION Return the $PSVersionTable as an object. .EXAMPLE Get-BuildPowerShellDetail Returns the $PSVersionTable as a PSObject. .OUTPUTS [PSObject] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-BuildOperatingSystemDetail .LINK Get-BuildEnvironment .LINK Get-BuildEnvironmentDetail #> [CmdletBinding()] [OutputType([PSObject])] Param () New-Object -TypeName PSObject -Property $PSVersionTable } #.ExternalHelp PSModuleBuildHelper-help.xml $script:ModuleBuildScriptFilename = 'Start-ModuleBuild.ps1' function Get-BuildScript { <# .SYNOPSIS Provides the full path to the module build script. .DESCRIPTION Provides the full path to the module build script that is held in the module folder. .EXAMPLE Get-BuildScript Returns the path to the Start-ModuleBuild.ps1 .OUTPUTS [System.IO.FileInfo] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : v1.0 - 15/03/18 - Initial release .LINK #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] Param() $rootPath = $PSScriptRoot if ((Split-Path -Path $rootPath -Leaf) -eq 'public') { $rootPath = Split-Path -Path $PSScriptRoot -Parent } # returns a [System.IO.FileInfo] object Get-Item -Path (Join-Path -Path $rootPath -ChildPath $script:ModuleBuildScriptFilename) } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-BuildSystem { <# .SYNOPSIS Gets the current build system. .DESCRIPTION Gets the current build system, such as AppVeyor, GitLab CI, Teamcity etc. If a build system cannot be detected (such as runnning on a local machine), then 'Unknown' will be returned. .EXAMPLE Get-BuildSystem Return the current build system. .OUTPUTS [String] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-BuildSystemEnvironment .LINK Get-BuildEnvironment #> [CmdletBinding()] [OutputType([String])] Param () # todo Make these values Enums for use elsewhere? $system = switch ((Get-Item env:).name) { 'APPVEYOR_BUILD_FOLDER' { 'AppVeyor'; break } 'GITLAB_CI' { 'GitLab CI' ; break } 'JENKINS_URL' { 'Jenkins'; break } 'BUILD_REPOSITORY_URI' { 'VSTS'; break } 'TEAMCITY_VERSION' { 'Teamcity'; break } 'BAMBOO_BUILDKEY' { 'Bamboo'; break } 'GOCD_SERVER_URL' { 'GoCD'; break } 'TRAVIS' { 'Travis CI'; break } } if (-not $system) { $system = 'Unknown' } $system } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-BuildSystemEnvironment { <# .SYNOPSIS Get the current build system environment variables. .DESCRIPTION Get the current build system environment variables. Primarily for use in a CI / CD build environments where it is useful to see. There is a default set of keywords that are searched for which can be replaced. The following CI / CD systems are supported: - AppVeyor - Unknown (retrieves ALL of the current environment variables) The data is returned as a PSObject. .EXAMPLE Get-BuildSystemEnvironment Returns the environment variables for the current build environment. .OUTPUTS [PSObject] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-BuildSystem .LINK Get-BuildEnvironment #> [CmdletBinding()] [OutputType([PSObject])] Param () $envVars = switch (Get-BuildSystem) { 'AppVeyor' { Get-Item env:APPVEYOR* } 'Unknown' { Get-Item env:* } } # return the environment as an object $envObject = New-Object -TypeName psobject $envVars.GetEnumerator() | ForEach-Object { $envObject | Add-Member -MemberType NoteProperty -Name $_.Name -Value $_.Value } $envObject } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-ChangelogVersion { <# .SYNOPSIS Gets the version from the changelog. .DESCRIPTION Gets the version from the changelog. .EXAMPLE Get-ChangelogVersion -Path 'c:\mymodule\chaneglog.md' This will return the first version listed in your changelog that matches the default regular expression. .EXAMPLE 'Value1', 'Value2' | <FUNCTION> .INPUTS [String] .OUTPUTS [PSObject] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-PowerShellGalleryVersion .LINK Get-ManifestVersion .LINK Get-NextReleaseVersion #> [OutputType([Version])] [CmdletBinding()] Param ( # Path to the changelog. [Parameter(Mandatory)] [ValidateScript ( { Test-Path $_ } )] [string] $Path, # Regular expression to match the version. This assumes that the format # of your changelog (in Markdown) versions are: # # 'hash'hash' v1.0.0 Some text # 'hash'hash' v0.9.0 Some more text # # This will then return the version as 1.0.0. Note that becasue the help # files are in Markdown I cannot type a double hash (which is a pound in # the US). So 'hash' is '#' [string] $VersionRegex = '##\s+v(\d+\.\d+\.\d+)' ) # get the version from the changelog.md if we have one switch -Regex -File ($Path) { $VersionRegex { return [version]$Matches[1] } } # end switch # if we get here we did not find the version [version]'0.0.0' } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-FunctionParameter { <# .SYNOPSIS Gets the parameters and their properties for a specified function. .DESCRIPTION Gets the parameters and their properties for a specified function. The function must exist within the 'Function:' provider so will not work on cmdlets. .EXAMPLE Get-FunctionParameters -Name 'Get-FunctionParameters' Returns the properties of each function parameter for the 'Import-Module' function excluding the common parameters for Advanced Functions. .EXAMPLE Get-FunctionParameters -Name 'Get-FunctionParameters' -Exclude '' Returns the properties of each function parameter for the 'Import-Module' function excluding no parameter names. .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 17/03/18 - Initial release #> [CmdletBinding()] Param ( # Name of the function. The function must exist within the 'Function:' # provider or an exception will be thrown. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Name, # Array of parameter names to exclude. By default the Advanced Functions # common parameters are excluded. Pass an empty array to have all # parameters returned. [AllowEmptyCollection()] [string[]] $Exclude = @('Verbose', 'Debug', 'ErrorAction', 'WarningAction', ` 'InformationAction', 'ErrorVariable', 'WarningVariable', 'InformationVariable', ` 'OutVariable', 'OutBuffer', 'PipelineVariable' ) ) if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } try { (Get-Item -Path "Function:\$Name").Parameters.GetEnumerator() | Where-Object { $Exclude -notcontains $_.key} } catch { throw "Cannot find function '$Name' loaded in the current session." } } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-GitBranchName { <# .SYNOPSIS Get the name of the current branch. .DESCRIPTION Get the name of the current branch. .EXAMPLE Get-GitBranchName Returns the current branch name for the current repo. .OUTPUTS [String] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK #> [CmdletBinding()] [OutputType([String])] Param () $branch = git rev-parse --abbrev-ref HEAD 2>&1 if (-not $?) { throw 'Cannot determine the current git branch.' } $branch } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-GitChange { <# .SYNOPSIS Gets the git unstaged changed files in the local .DESCRIPTION Gets a list of hte changed files in the local repository. Only the file names are returned not their change status (deleted, modified, unstaged etc.) .EXAMPLE Get-GitChange Gets the lilst of git changed files in the current repository. .OUTPUTS [String[]] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "", Justification = "This will eventually be rewritten")] [CmdletBinding()] [OutputType([String[]])] Param () # todo this really needs rewritten to use something other than the local git command @(Invoke-Expression -Command 'git status -s') | ForEach-Object { if ($_ -match '\S*$') { $matches[0] } } } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-GitLastCommitHash { <# .SYNOPSIS Gets the hash of the last commit. .DESCRIPTION Gets the hash of the last commit. .EXAMPLE Get-GitLastCommitHash Gets the hash of hte last commit for the current branch and repo. .OUTPUTS [String] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK #> [CmdletBinding()] [OutputType([String])] param () $hash = git log -1 --pretty=%H 2>&1 if (-not $?) { throw 'There are no commits.' } else { ($hash | Where-Object { $_ } | Out-String).Trim() } } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-GitLastCommitMessage { <# .SYNOPSIS Getsh the last Git commit message. .DESCRIPTION Gets the last Git commit message. If there are no messages to retrieve then it will throw and exception. Uses the 'git' command to retrieve the messages so this must be installed. .EXAMPLE Get-GitLastCommitMessage Returns the last commit message for the current branch and repo. .OUTPUTS [String] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK #> [CmdletBinding()] [OutputType([String])] Param () $message = git log -1 --pretty=%B 2>&1 if (-not $?) { throw 'There are no commit messages.' } else { ($message | Where-Object { $_ } | Out-String).Trim() } } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-ManifestVersion { <# .SYNOPSIS Gets the version from the module manifest. .DESCRIPTION Gets the version from the module manifest. .EXAMPLE Get-ManifestVersion -Path 'c:\temp\mymodule.psd1' .OUTPUTS [Version] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-PowerShellGalleryVersion .LINK Get-NextReleaseVersion .LINK Get-ChangelogVersion #> [OutputType([Version])] [CmdletBinding()] Param ( [Parameter(Mandatory)] [ValidateScript( { Test-Path -Path $_})] [string]$Path ) if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } # get the version from the manifest try { Write-Verbose "Getting the version from the manifest at '$Path'." [version](Get-MetaData -Path $Path -PropertyName ModuleVersion -ErrorAction Stop) } catch { [version]'0.0.0' } } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-NextReleaseVersion { <# .SYNOPSIS Get's the next release version. .DESCRIPTION Get's the next release version. .EXAMPLE Get-NextReleaseVersion -LatestVersion [Version]'1.0.0' -ReleaseType 'Minor' Will return a new version number of [Version]'1.1.0' which is a minor version increase from '1.0.0' .OUTPUTS [Version] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-PowerShellGalleryVersion .LINK Get-ManifestVersion .LINK Get-ChangelogVersion #> [OutputType([Version])] [CmdletBinding()] Param ( # The latest releaase version. [Parameter(Mandatory)] [Version] $LatestVersion, # The type of this release. [Parameter(Mandatory)] [String] $ReleaseType ) if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } Write-Verbose "Release type is $($ReleaseType)." $version = switch ($ReleaseType) { Major { Write-Verbose "Incrementing major version number." New-Object Version(($LatestVersion.Major + 1), 0, 0) } Minor { Write-Verbose "Incrementing minor version number." New-Object Version($LatestVersion.Major, ($LatestVersion.Minor + 1), 0) } Build { Write-Verbose "Incrementing build version number." New-Object Version($LatestVersion.Major, $LatestVersion.Minor, ($LatestVersion.Build + 1)) } default { # this also catches the ReleaseType of None Write-Verbose 'Not incrementing any version numbers.' New-Object Version($LatestVersion) } } Write-Verbose "New version will be $version" $version } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-PowerShellGalleryVersion { <# .SYNOPSIS Gets the version of the module in the POwerShell Gallery. .DESCRIPTION Gets the version of the module in the POwerShell Gallery. .EXAMPLE Get-PowerShellGalleryVersion -Name 'MyModule' Gets the latest version of the module 'mymodule' listed in the PowerShell Gallery. .OUTPUTS [Version] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-NextReleaseVersion .LINK Get-ManifestVersion .LINK Get-ChangelogVersion #> [OutputType([Version])] [CmdletBinding()] Param ( # Name of the module. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Name ) if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } # get the version from the PowerShell Gallery try { Write-Verbose "Getting the latest version of '$Name' from the PowerShell Gallery." [version](Find-Module -Name $Name -ErrorAction Stop).Version } catch { Write-Verbose "Did not find module '$Name' in the PowerShell Gallery." [version]'0.0.0' } } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-ProjectEnvironment { <# .SYNOPSIS Get the properties of the project environment. .DESCRIPTION Get the properties of the project in the current location. .EXAMPLE Get-ProjectEnvironment Gets the project environment for the current location. .OUTPUTS [PSObject] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release The idea came from the Indented.Build project (https://github.com/indented-automation/Indented.Build) and heavily modified. Credit should be given to that project. .LINK Get-BuildEnvironment .LINK Get-TestEnvironment #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseConsistentWhitespace", "", Justification = "Causes issue with the large hash table")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAlignAssignmentStatement", "", Justification = "Causes issue with the large hash table")] [OutputType([PSObject])] [CmdletBinding()] Param () if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } $projectRoot = Get-ProjectRoot $sourcePath = Get-SourcePath -ProjectRoot $projectRoot # test path try { $testPath = (Get-ChildItem (Join-Path -Path $projectRoot -ChildPath 'test*') -Directory | ` Select-Object -First 1).ToString() } catch { Write-Warning 'We have no tests folder!' } [PSCustomObject]@{ ModuleName = Split-Path -Path $projectRoot -Leaf BuildSystem = Get-BuildSystem ProjectRootPath = $projectRoot SourcePath = $sourcePath BuildRootPath = Join-Path -Path $projectRoot -ChildPath 'buildoutput' OutputPath = Join-Path -Path $projectRoot -ChildPath 'output' TestPath = $testPath } } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-ProjectRoot { <# .SYNOPSIS Gets the project root folder name. .DESCRIPTION Gets the project root folder name by looking for the git repo root folder. It uses the 'git' executable to do this so it must be installed. .EXAMPLE Get-ProjectRoot Returns the root folder for this project / git repository. .OUTPUTS [String] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release This function was lifted from the Indented.Build project (https://github.com/indented-automation/Indented.Build) and all credit for it should go to that project. .LINK Get-SourcePath #> [CmdletBinding()] [OutputType([String])] Param () $path = git rev-parse --show-toplevel if ($null -eq $path) { throw 'Not a git repository - cannot provide project root' } else { (Get-Item $path).FullName } } function Get-ReleaseType { <# .SYNOPSIS Gets the release type from the last commit message. .DESCRIPTION Gets the release type from the last commit message. The message should start with: - 'Major release' : 'Major' release type - 'Minor release' : 'Minor' release type - 'Release' : 'Build' release type - None of the above : 'None' release type .EXAMPLE Get-ReleaseType -CommitMessage '' Gets' the release type ofr an empty commit message - this will be 'None' .OUTPUTS [String] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-GitLastCommitMessage .LINK Get-GitLastCommitHash .LINK Get-GitBranchName #> [OutputType([String])] [CmdletBinding()] Param ( # The commit message to be used to determine the release type. # The message should start with: # # - 'Major release' : 'Major' release type # - 'Minor release' : 'Minor' release type # - 'Release' : 'Build' release type # - None of the above : 'None' release type [Parameter(Mandatory)] [AllowEmptyString()] [string]$CommitMessage ) if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } Write-Verbose "Commit message: $CommitMessage" switch -Regex ($CommitMessage) { '^Major release' { Write-Verbose "Commit message contains 'Major release'. ReleaseType is 'Major'." 'Major' } '^Minor release' { Write-Verbose "Commit message starts with 'Minor release'. Release type is 'Minor'." 'Minor' } '^Release' { Write-Verbose "Commit message contains 'Release'. Release type is 'Build'" 'Build' } default { Write-Verbose "Commit message does not contain any release types. Release type is 'None'" 'None' } } } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-SourcePath { <# .SYNOPSIS Gets the source path of the module. .DESCRIPTION Gets the source path of the module under the project root. The source path can be called any one of the following: - source - src - <NAME>\<NAME>.psd1 Note that the last one is simply a parent folder whose name is the same name as the manifest within it. .EXAMPLE Get-SourcePath -Path 'c:\mymodule' Gets the path to the module source under 'c:\mymodule' .OUTPUTS [System.IO.DirectoryInfo], [System.IO.DirectoryInfo[]] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release Note that this function was lifted from teh Indented.Build module (https://github.com/indented-automation/Indented.Build) and modified to allow 'source' and 'src' folders to be used and replaced Push- and Pop-Location with Set-Location as Invoke-Build recommends not using those cmdlets. All credit should be given to that project. .LINK Get-BuildSystem .LINK Get-BuildEnvironment .LINK Get-ProjectRoot #> [CmdletBinding()] [OutputType([String])] Param ( # Root path for the project. [Parameter(Mandatory, ValueFromPipeline)] [String]$ProjectRoot ) if (Test-Path (Join-Path -Path $ProjectRoot -ChildPath (Split-Path -Path $ProjectRoot -Leaf))) { return (Join-Path -Path $ProjectRoot -ChildPath (Split-Path -Path $ProjectRoot -Leaf)) } else { $folders = 'src', 'source' ForEach ($folder in $folders) { $sourcePath = Join-Path -Path $ProjectRoot -ChildPath $folder if (Test-Path $sourcePath) { return $sourcePath } } #end foreach-object } # end else # we get here we have found nothing throw 'Unable to determine the source path' } #.ExternalHelp PSModuleBuildHelper-help.xml function Get-TestEnvironment { <# .SYNOPSIS Get the properties of the testing environment. .DESCRIPTION Get the properties required to test the project, or elements of the project. Some of the paths require that the module already be built beforehand and will throw an exception if that is not the case. All areas of the test process takes it's details from the data produced by this function. To influence part of the build process the data produced only need be altered. Note that BuildPath, BuildManifestPath and BuildModulePath use the latest build directory based on it's version number. This funciton assumes that the module has already been built .EXAMPLE Get-TestEnvironment Get test information for the current or any child directories. .OUTPUTS [PSObject] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release The idea came from the Indented.Build project (https://github.com/indented-automation/Indented.Build) and heavily modified. Credit should be given to that project. .LINK Get-ProjectEnvironment .LINK Get-BuildEnvironment #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseConsistentWhitespace", "", Justification = "Causes issue with the large hash table")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAlignAssignmentStatement", "", Justification = "Causes issue with the large hash table")] [OutputType([PSObject])] [CmdletBinding()] Param ( # Filename of the PowerShell ScriptAnalyzer settings file. This file # will be searched for under the project root. [string] $PSSASettingsName = 'PSScriptAnalyzerSettings.psd1', # The PowerShell ScriptAnalyzer Custom Rules folder. This folder will be # searched for under the project root and any .psd1 files found under # teh folder will be used. [string] $PSSACustomRulesFolderName = 'CustomAnalyzerRules' ) if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } $testInfo = Get-ProjectEnvironment $buildOutput = Join-Path -Path $testInfo.ProjectRootPath -ChildPath 'buildoutput' # get all of the versions built in the 'buildoutput' folder, and sort them # by name and choose the latest one - to sort them by name properly we have # to convert the names to versions (as 0.10.0 comes before 0.9.0 when using # strings) $latestBuildVersion = (Get-Childitem $buildOutput | ` Select-Object -Property @{ l = 'Name'; e = { [version]$_.Name } } | Sort-Object Name -Descending | ` Select-Object -First 1).Name.ToString() if ($latestBuildVersion -eq '') { throw 'Cannot find the latest build of the module. Did you build it beforehand?' } @( @{ name = 'SourceManifestPath' value = (Join-Path -Path $testInfo.SourcePath -ChildPath "$($testInfo.ModuleName).psd1") }, @{ name = 'SourceModulePath' value = (Join-Path -Path $testInfo.SourcePath -ChildPath "$($testInfo.ModuleName).psm1") }, @{ name = 'BuildPath' value = (Join-Path -Path $buildOutput -ChildPath $latestBuildVersion) }, @{ name = 'BuildManifestPath' value = (Join-Path -Path (Join-Path $buildOutput -ChildPath $latestBuildVersion) -ChildPath "$($testInfo.ModuleName).psd1") }, @{ name = 'BuildModulePath' value = (Join-Path -Path (Join-Path $buildOutput -ChildPath $latestBuildVersion) -ChildPath "$($testInfo.ModuleName).psm1") } @{ name = 'PSSASettingsPath' value = '' }, @{ name = 'PSSACustomRulesPath' value = '' } ) | ForEach-Object { $testInfo | Add-Member -MemberType NoteProperty -Name $_.Name -Value $_.Value } # PSSA $settingsPath = Get-ChildItem $PSSASettingsName -Recurse | Select-Object -First 1 if ($settingsPath) { $testInfo.PSSASettingsPath = $settingsPath.FullName } else { Write-Verbose "Could not find PSScriptAnalyzer Settings file '$PSSASettingsName'." } $path = Get-ChildItem -Path $PSSACustomRulesFolderName -Directory -Recurse | Select-Object -First 1 if ($path -and (Test-Path -Path (Join-Path -Path $path.FullName -ChildPath '*.psd1'))) { $testInfo.PSSACustomRulesPath = $path.FullName } else { Write-Verbose "No PSScriptAnalyzer Custom Rules folder '$PSSACustomRulesFolderName' found." } $testInfo } #.ExternalHelp PSModuleBuildHelper-help.xml function Hide-SensitiveData { <# .SYNOPSIS Searches the object property names for keywords and masks their value if it matches. .DESCRIPTION Searches the object property names for keywords and masks their value if it matches. There is a default set of keywords that are searched for which can be replaced. .EXAMPLE $test = New-Object -TypeName PSObject -Property @{ api = 'abcd', name = 'Luke' } $test | Hide-SensitiveData Returns the object with the value of 'api' as '*****' .OUTPUTS [PSObject] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release .LINK Get-BuildSystem .LINK Get-BuildEnvironment #> [CmdletBinding()] [OutputType([PSObject])] Param ( # Object to search the keys for matching keywords. Cannot be $null or empty. [Parameter(Mandatory, ValueFromPipeline)] [ValidateScript( { $null -ne $_ -and @($_.psobject.properties).count -ne 0 } )] [PSObject] $InputObject, # Array of regular expressions to match the keys against. [String[]] $Keyword = @('password', 'secret', 'key', 'api', 'token'), # The mask that will be used to replace any matching keyword values. [String] $Mask = '[protected]' ) begin { if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } } process { # objects are passed by reference so any changes to it are made to the original # clone the input object so we don't change it $copyObj = New-Object -TypeName PsObject $InputObject.PSObject.Properties | ForEach-Object { $copyObj | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $_.Value } # filter the environment secret vars # if the environment variable is empty then don't mask it ForEach ($obj in $copyObj.PSObject.Properties) { Write-Debug "Checking '$($obj.Name)' for a keyword match." ForEach ($k in $Keyword) { # does the name match the secret and it's not empty if ($obj.Name -match $k) { $obj.Value = $Mask Write-Verbose "Key matched keyword '$k'. Value changed to '$($obj.Value)'." break } } } } end { $copyObj } } #.ExternalHelp PSModuleBuildHelper-help.xml function Initialize-TestEnvironment { <# .SYNOPSIS Brief synopsis about the function. .DESCRIPTION Detailed explanation of the purpose of this function. .PARAMETER Param1 The purpose of param1. .PARAMETER Param2 The purpose of param2. .EXAMPLE <FUNCTION> -Param1 'Value1', 'Value2' .EXAMPLE 'Value1', 'Value2' | <FUNCTION> .INPUTS [String] .OUTPUTS [PSObject] .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 16/03/18 - Initial release .LINK Related things #> [CmdletBinding()] Param() if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } $testInfo = Get-TestEnvironment if ($testInfo.BuildSystem -eq 'Unknown') { # re-import the module each time if you are running it locally - using # the SuppressImportModule will only import it once each session Write-Verbose "Removing module '$($testInfo.Modulename)'." Remove-Module $testInfo.ModuleName -Force Write-Verbose "Importing module '$($testInfo.BuildManifestPath)'" Import-Module -FullyQualifiedName $testInfo.BuildManifestPath -Force } elseif (-not (Get-Module -Name $testInfo.ModuleName -ErrorAction SilentlyContinue) -or !(Test-Path Variable:SuppressImportModule) -or !$SuppressImportModule) { # The first time this is called, the module will be forcibly (re-)imported. # After importing it once, the $SuppressImportModule flag should prevent # the module from being imported again for each test file. # -Scope Global is needed when running tests from within a CI environment Import-Module -FullyQualifiedName $testInfo.BuildManifestPath -Scope Global -Force # Set to true so we don't need to import it again for the next test $Script:SuppressImportModule = $true } $testInfo } #.ExternalHelp PSModuleBuildHelper-help.xml function New-GithubRelease { <# .SYNOPSIS Creates a release on GitHub. .DESCRIPTION Creates a release on GitHub using an already created artifact. Note that this does not use the credential store for authentication but uses the GitHub Username and Api Key passed to the function. .EXAMPLE New-GitHubRelease -Version 1.0 -CommitId 'a6fe432' -ArtifactPath 'c:\temp\mymodule-1.0.zip' -GitHubUsername 'me' -GitHubRepository 'mymodule' -GitHubApiKey '123456789' This will create a new version 1.0 relase on GitHub for the Commit ID 'a6fe432' using the artifact 'c:\temp\mymodule-1.0.zip' in the GitHub repository 'mymodule' using the Api Key and Username for authentication. .NOTES Author : Paul Broadwith (https://github.com/pauby) History : 1.0 - 15/03/18 - Initial release #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseConsistentWhitespace", "", Justification = "Causes issue with the open hash tables")] [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] Param ( # The version number to be used for this release. [Parameter(Mandatory)] [Version]$Version, # The Commit ID corresponding to this release. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$CommitID, # Release notes for this release. [string]$ReleaseNotes = '', # The path to the release artifact. [Parameter(Mandatory)] [ValidateScript( { Test-Path $_ } )] [string]$ArtifactPath, # The GitHub Username to use for authentication. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $GitHubUsername, # The Github API key used for authentication. # See (https://github.com/blog/1509-personal-api-tokens) [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $GitHubApiKey, # Which GitHub repository this release is for. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $GitHubRepository, # Marks this release as a draft. [Switch] $DraftRelease, # Marks this release as a pre-release. [switch] $PreRelease ) if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference') } # Get just the name of the file to attach to this release $artifact = Split-Path $ArtifactPath -Leaf $releaseData = @{ tag_name = "v{0}" -f $Version target_commitish = $CommitId name = "v{0}" -f $Version body = $ReleaseNotes draft = $DraftRelease.IsPresent prerelease = $PreRelease.IsPresent } $releaseParams = @{ Uri = "https://api.github.com/repos/$GitHubUsername/$GitHubRepository/releases" Method = 'POST' Headers = @{ Authorization = 'Basic ' + [Convert]::ToBase64String( [Text.Encoding]::ASCII.GetBytes($GitHubApiKey + ":x-oauth-basic")) } ContentType = 'application/json' Body = (ConvertTo-Json $releaseData -Compress) UseBasicParsing = $true } # force use of TLS 1.2 Write-Verbose 'Forcing using of TLS1.2 for GitHub.' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Write-Verbose 'Creating tagged Github release.' try { $result = Invoke-RestMethod @releaseParams -OutVariable $errorMsg } catch { throw "Could not create tagged GitHub release - '$_'" } $uploadUri = $result | Select-Object -ExpandProperty upload_url $uploadUri = $uploadUri -replace '\{\?name.*\}', "?name=$artifact" $uploadParams = @{ Uri = $uploadUri Method = 'POST' Headers = @{ Authorization = 'Basic ' + [Convert]::ToBase64String( [Text.Encoding]::ASCII.GetBytes($GitHubApiKey + ":x-oauth-basic")); } ContentType = 'application/zip' InFile = $ArtifactPath } if ($PSCmdlet.ShouldProcess("ShouldProcess?")) { Write-Verbose 'Uploading artifact.' $response = Invoke-RestMethod @uploadParams Write-Verbose "Response from artifact upload: $response" } } |