Build/Build.PSake.ps1
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingCmdletAliases", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [cmdletbinding()] param() # PSake makes variables declared here available in other scriptblocks Properties { $ProjectRoot = $ENV:BHProjectPath if (-not $ProjectRoot) { $ProjectRoot = $PSScriptRoot } $Timestamp = Get-Date -UFormat '%Y%m%d-%H%M%S' $PSVersion = $PSVersionTable.PSVersion.Major $lines = '----------------------------------------------------------------------' # Pester $TestScripts = Get-ChildItem "$ProjectRoot\Tests\*\*Tests.ps1" $TestFile = "Test-Unit_$($TimeStamp).xml" # Script Analyzer [ValidateSet('Error', 'Warning', 'Any', 'None')] $ScriptAnalysisFailBuildOnSeverityLevel = 'Error' $ScriptAnalyzerSettingsPath = "$ProjectRoot\Build\PSScriptAnalyzerSettings.psd1" # Build $ArtifactFolder = Join-Path -Path $ProjectRoot -ChildPath 'BuildOutput' # Staging $StagingFolder = Join-Path -Path $ProjectRoot -ChildPath 'Staging' $StagingModulePath = Join-Path -Path $StagingFolder -ChildPath $env:BHProjectName $StagingModuleManifestPath = Join-Path -Path $StagingModulePath -ChildPath "$($env:BHProjectName).psd1" # Documentation $DocumentationPath = Join-Path -Path $StagingModulePath -ChildPath 'Documentation' } FormatTaskName "-------- {0} --------" Task 'FullAzure' -Depends 'Init', 'UpdateBuildVersion', 'ImportStagingModule', 'Stage', 'Help', 'Test', 'UpdateRepo', 'PublishAzure' Task 'FullPSGallery' -Depends 'Init', 'UpdateBuildVersion', 'Stage', 'ImportStagingModule', 'Help', 'Test', 'UpdateRepo', 'PublishPSGallery' # Define top-level tasks Task 'Default' -Depends 'Init' # Show build variables Task Init { #$lines Write-Output "Settng up Staging and BuildOutput folders in .gitignore`n" Set-Location $ProjectRoot if((Test-Path $($ProjectRoot + $env:BHPathDivider + ".gitignore")) -eq $true){ #Add Folders to gitignore. You don't need these in your repo $file = Get-Content $($ProjectRoot + $env:BHPathDivider + ".gitignore") $containsWord = $file | % { $_ -match "Staging|BuildOutput" } if ($containsWord -notcontains $true) { Add-Content -Path .gitignore -Value "Staging/" Add-Content -Path .gitignore -Value "BuildOutput/" } } if (-not(Test-Path $($ProjectRoot + $env:BHPathDivider + $env:BHBuildCulture))){ New-Item -path $ProjectRoot -Name $env:BHBuildCulture -ItemType Directory } "`n" "Build System Details:`n" Get-Item ENV:BH* "`n" } # Clean the Artifact and Staging folders Task 'Clean' -Depends 'Init' { #$lines Write-Output "Resetting Staging and BuildOutput Folders`n" $foldersToClean = @( $ArtifactFolder $StagingFolder ) # Remove folders foreach ($folderPath in $foldersToClean) { Remove-Item -Path $folderPath -Recurse -Force -ErrorAction 'SilentlyContinue' New-Item -Path $folderPath -ItemType 'Directory' -Force | Out-String | Write-Verbose } } # Create a single .psm1 module file containing all functions # Copy new module and other supporting files (Documentation / Examples) to Staging folder Task 'CombineFunctionsAndStage' -Depends 'Clean' { #$lines Write-Output "Combining Functions into one PSM1 and Staging`n" # Create folders New-Item -Path $StagingFolder -ItemType 'Directory' -Force | Out-String | Write-Verbose New-Item -Path $StagingModulePath -ItemType 'Directory' -Force | Out-String | Write-Verbose # Get public and private function files $publicFunctions = @( Get-ChildItem -Path "$env:BHModulePath\Public\*.ps1" -Recurse -ErrorAction 'SilentlyContinue' ) $privateFunctions = @( Get-ChildItem -Path "$env:BHModulePath\Private\*.ps1" -Recurse -ErrorAction 'SilentlyContinue' ) # Combine functions into a single .psm1 module $combinedModulePath = Join-Path -Path $StagingModulePath -ChildPath "$($env:BHProjectName).psm1" @($publicFunctions + $privateFunctions) | Get-Content | Add-Content -Path $combinedModulePath # Copy other required folders and files $pathsToCopy = @( Join-Path -Path $ProjectRoot -ChildPath 'Documentation' Join-Path -Path $ProjectRoot -ChildPath 'Examples' # Join-Path -Path $ProjectRoot -ChildPath 'CHANGELOG.md' Join-Path -Path $ProjectRoot -ChildPath 'README.md' ) Copy-Item -Path $pathsToCopy -Destination $StagingFolder -Recurse # Copy existing manifest Copy-Item -Path $env:BHPSModuleManifest -Destination $StagingModulePath -Recurse } # Create a folder structure containing Public, Private and whatever else folders # Copy new module and other supporting files (Documentation / Examples) to Staging folder Task 'Stage' -Depends 'Clean' { #$lines Write-Output "Building a Module folder at: [$StagingModulePath]`n" # Create folders New-Item -Path $StagingFolder -ItemType 'Directory' -Force | Out-String | Write-Verbose New-Item -Path $StagingModulePath -ItemType 'Directory' -Force | Out-String | Write-Verbose "`n" Write-Output "Staging Module Path is: $StagingModulePath " Write-Output "Staging Folder Path is: $StagingFolder" # Copy required folders and files $pathsToCopy = @( Join-Path -Path $ProjectRoot -ChildPath $env:BHBuildCulture Join-Path -Path $ProjectRoot -ChildPath 'Documentation' Join-Path -Path $ProjectRoot -ChildPath 'Build' Join-Path -Path $ProjectRoot -ChildPath 'Certs' Join-Path -Path $ProjectRoot -ChildPath 'PlasterTemplate' Join-Path -Path $ProjectRoot -ChildPath 'Scaffold' Join-Path -Path $ProjectRoot -ChildPath 'Spec' Join-Path -Path $ProjectRoot -ChildPath 'Public' Join-Path -Path $ProjectRoot -ChildPath 'Private' Join-Path -Path $ProjectRoot -ChildPath 'Tests' Join-Path -Path $ProjectRoot -ChildPath 'PSNow.nuspec' Join-Path -Path $ProjectRoot -ChildPath 'PSNow.psm1' Join-Path -Path $ProjectRoot -ChildPath 'PSNow.psd1' Join-Path -Path $ProjectRoot -ChildPath 'readme.md' Join-Path -Path $ProjectRoot -ChildPath 'LICENSE.md' ) Copy-Item -Path $pathsToCopy -Destination $StagingModulePath -Recurse } # Import new module Task 'ImportStagingModule' -Depends 'Init' { #$lines Write-Output "Reloading staged module from path: [$StagingModulePath]`n" # Reload module if (Get-Module -Name $env:BHProjectName) { Remove-Module -Name $env:BHProjectName } # Global scope used for Help creation (PlatyPS) Import-Module -Name $StagingModulePath -ErrorAction 'Stop' -Force -Global } # Run PSScriptAnalyzer against code to ensure quality and best practices are used Task 'Analyze' -Depends 'ImportStagingModule' { #$lines Write-Output "Running PSScriptAnalyzer on path: [$StagingModulePath]`n" $Results = Invoke-ScriptAnalyzer -Path $StagingFolder -Recurse -Settings $ScriptAnalyzerSettingsPath -Verbose:$VerbosePreference #$Results | Select-Object 'RuleName', 'Severity', 'ScriptName', 'Line', 'Message' | Format-List $Results | Select-Object 'RuleName', 'Severity', 'ScriptName', 'Line', 'Message' | foreach { if($_.Severity -eq 'Error'){ [console]::ForegroundColor = 'Red'; $_; } elseif ($_.Severity -eq 'Warning') { [console]::ForegroundColor = 'Yellow'; $_; } else { $_ } } | Format-List [console]::ForegroundColor = 'White' switch ($ScriptAnalysisFailBuildOnSeverityLevel) { 'None' { return } 'Error' { Assert -conditionToCheck ( ($Results | Where-Object 'Severity' -eq 'Error').Count -eq 0 ) -failureMessage 'One or more ScriptAnalyzer errors were found. Build cannot continue!' } 'Warning' { Assert -conditionToCheck ( ($Results | Where-Object { $_.Severity -eq 'Warning' -or $_.Severity -eq 'Error' }).Count -eq 0) -failureMessage 'One or more ScriptAnalyzer warnings were found. Build cannot continue!' } default { Assert -conditionToCheck ($analysisResult.Count -eq 0) -failureMessage 'One or more ScriptAnalyzer issues were found. Build cannot continue!' } } } # Run Pester tests # Unit tests: verify inputs / outputs / expected execution path # Misc tests: verify manifest data, check comment-based help exists Task 'Test' -Depends 'ImportStagingModule' { #$lines Write-Output "Running Tests against the module`n" # PSScriptAnalyzer doesn't ignore files, only rules. Temporarily renaming files here which can safely skip Linting $directoriestoexclude = @('Spec' <#,'Scaffold'#>) foreach($directory in $directoriestoexclude){ $insidepath = $env:BHModulePath + $env:BHPathDivider + $directory $filestorename = @( Get-ChildItem -Path "$insidepath/*.ps1" -Recurse -ErrorAction 'SilentlyContinue' ) foreach($file in $filestorename){ $newname = $file.Name + ".hold" Rename-Item -path $file.PSPath -NewName $newname } } # Gather test results. Store them in a variable and file $TestFilePath = Join-Path -Path $StagingFolder -ChildPath $TestFile $TestResults = Invoke-Pester -Script $TestScripts -PassThru -OutputFormat 'NUnitXml' -OutputFile $TestFilePath -PesterOption @{IncludeVSCodeMarker = $true } # Fail build if any tests fail if ($TestResults.FailedCount -gt 0) { Write-Error "Failed '$($TestResults.FailedCount)' tests, build failed" } # PSScriptAnalyzer doesn't ignore files, only rules. Renaming the files back again foreach ($directory in $directoriestoexclude) { $insidepath = $env:BHProjectPath + $env:BHPathDivider + $directory $filestorename = @( Get-ChildItem -Path "$insidepath/*.hold" -Recurse -ErrorAction 'SilentlyContinue' ) foreach ($file in $filestorename) { $newname = (Get-Item $file).Basename Rename-Item -path $file.PSPath -NewName $newname } } } # Create new Documentation markdown files from comment-based help Task 'Help' { #$lines Write-Output "Updating Markdown help in Staging folder: [$DocumentationPath]`n" # $null = Import-Module -Name $env:BHPSModuleManifest -Global -Force -PassThru -Verbose # Cleanup Remove-Item -Path $DocumentationPath -Recurse -Force -ErrorAction 'SilentlyContinue' Start-Sleep -Seconds 5 New-Item -Path $DocumentationPath -ItemType 'Directory' | Out-Null # Create new Documentation markdown files $platyPSParams = @{ Module = $env:BHProjectName OutputFolder = $DocumentationPath NoMetadata = $false } New-MarkdownHelp @platyPSParams -ErrorAction 'SilentlyContinue' -Verbose | Out-Null Copy-Item -Path $($DocumentationPath + $env:BHPathDivider + "*.*") -Destination $($env:BHProjectPath + $env:BHPathDivider + "Documentation") -Force # Create the XML help file which you need when calling get-help mymodule Write-Output "Now updating External Help for the Module in [$StagingModulePath]" New-ExternalHelp $DocumentationPath -OutputPath $($env:BHBuildCulture + $env:BHPathDivider) -force # Sync help files from the staging folder back to the parent folder Copy-Item -Path $($StagingModulePath + $env:BHPathDivider + $env:BHBuildCulture + $env:BHPathDivider + "*.*") -Destination $($env:BHProjectPath + $env:BHPathDivider + $env:BHBuildCulture) -Force # Update index.md Write-Output "Copying index.md...`n" Copy-Item -Path $($env:BHProjectPath + $env:BHPathDivider + "README.md") -Destination $($DocumentationPath + $env:BHPathDivider + "index.md") -Force -Verbose | Out-Null } Task 'UpdateBuildVersion' { #$lines Write-Output "Updating the Module Version`n" $manifest = Import-PowerShellDataFile $env:BHPSModuleManifest [version]$Version = $manifest.ModuleVersion switch ( $BuildRev ) { Major { [version]$NewVersion = "{0}.{1}.{2}.{3}" -f ($Version.Major + 1), $Version.Minor, $Version.Build, $version.Revision } Minor { [version]$NewVersion = "{0}.{1}.{2}.{3}" -f $Version.Major, ($Version.Minor + 1), $Version.Build, $version.Revision } Build { [version]$NewVersion = "{0}.{1}.{2}.{3}" -f $Version.Major, $Version.Minor, ($Version.Build + 1), $version.Revision } Revision { [version]$NewVersion = "{0}.{1}.{2}.{3}" -f $Version.Major, $Version.Minor, $Version.Build, ($version.Revision + 1) } None { [version]$NewVersion = $env:BHBuildNumber} } Update-ModuleManifest -Path $env:BHPSModuleManifest -ModuleVersion $NewVersion Set-Item -Path Env:BHBuildNumber -Value $NewVersion $MonolithFile = $($env:BHProjectPath + $env:BHPathDivider + "$env:BHProjectName.nuspec") #Create a new file and Update each time. $xmlFile = New-Object xml $xmlFile.Load($MonolithFile) #Set the version to the one that is in the manifest. $xmlFile.package.metadata.version = $NewVersion.ToString() $xmlFile.Save($MonolithFile) #exec { git commit $manifest -m "Updated the module version" } } Task 'UpdateRepo' -Depends 'Init' { #$lines Write-Output "Updating the repository`n" if ( ($env:BHCommitFlag -eq 0) -or ( (Test-Path -Path Env:BHCommitFlag) -eq $false ) ) { $results = $false } else { $results = $true } Assert -conditionToCheck $results -failureMessage 'Please pass in a commit message. Build cannot continue!' #does my current build number match what I already pushed to git? If yes, don't tag $gittagversion = git tag --merged if ($gittagversion.Contains("v" + "$env:BHBuildNumber") ) { Exec { git add -u } Exec { git commit -m $env:BHCommitMessage } Exec { git push } Exec { git push origin $env:BHBranchName } } else { Exec { git add -u } Exec { git commit -m $env:BHCommitMessage } Exec { git push } Exec { git tag -a "v$env:BHBuildNumber" -m "v$env:BHBuildNumber" } Exec { git push origin $env:BHBranchName } } Set-Item -Path Env:BHCommitFlag -Value 0 } # https://www.mono-project.com/docs/about-mono/supported-platforms/macos/ Task 'BuildNuget' -Depends ImportStagingModule { #$lines #test for staged files and error out if they don't exist Write-Output "Creating a Nuget Package in the Aritfacts folder: [$ArtifactFolder]`n" $NuSpecFile = $env:BHProjectPath + $env:BHPathDivider + "$env:BHProjectName.nuspec" #Test to see if the Authors field has been properly updated $loadedfile = Get-Content -Raw -Path $NuSpecFile if ($loadedfile -match 'Your') { throw "Your NuSpec file -> [$NuSpecFile] is not updated correctly. Please add your name to the Authors line and don't forget to remove the < > as well" } if ($env:BHPSVersionNumber -lt 6) { exec { nuget pack $($env:BHModulePath + $env:BHPathDivider + $env:BHProjectName.nuspec) -Version $($env:BHBuildNumber) -NoDefaultExcludes } } elseif (Get-Variable -Name 'IsWindows' -ErrorAction 'SilentlyContinue' -ValueOnly ) { exec { nuget pack $($env:BHModulePath + $env:BHPathDivider + $env:BHProjectName.nuspec) -Version $($env:BHBuildNumber) -NoDefaultExcludes } } elseif (Get-Variable -Name 'IsMacOS' -ErrorAction 'SilentlyContinue' -ValueOnly ) { exec { bash -c "nuget pack -Version $($env:BHBuildNumber) -NoDefaultExcludes" } } elseif (Get-Variable -Name 'IsLinux' -ErrorAction 'SilentlyContinue' -ValueOnly ) { exec { bash -c "nuget pack -Version $($env:BHBuildNumber) -NoDefaultExcludes" } } $newpackagename = $env:BHProjectName + "." + $env:BHBuildNumber + ".nupkg" Move-Item -Path $newpackagename -Destination $ArtifactFolder Write-Output "`nFINISHED: Nuget artifact creation." } # Create a versioned zip file of all staged files # NOTE: Admin Rights are needed if you run this locally Task 'BuildZip' -Depends ImportStagingModule { #$lines Write-Output "`nCreating a Build Artifact" # Create /Release folder New-Item -Path $ArtifactFolder -ItemType 'Directory' -Force | Out-String | Write-Verbose # Get current manifest version try { $manifest = Test-ModuleManifest -Path $StagingModuleManifestPath -ErrorAction 'Stop' [Version]$manifestVersion = $manifest.Version } catch { throw "Could not get manifest version from [$StagingModuleManifestPath]" } # Create zip file try { $releaseFilename = "$($env:BHProjectName)-v$($manifestVersion.ToString()).zip" $releasePath = Join-Path -Path $ArtifactFolder -ChildPath $releaseFilename Write-Host "Creating release artifact [$releasePath] using manifest version [$manifestVersion]" -ForegroundColor 'Yellow' Compress-Archive -Path $($StagingFolder + $env:BHPathDivider + "*") -DestinationPath $releasePath -Force -Verbose -ErrorAction 'Stop' } catch { throw "Could not create release artifact [$releasePath] using manifest version [$manifestVersion]" } Write-Output "`nFINISHED: Release artifact creation." } Task 'PublishAzure' -Depends 'BuildNuget' { #$lines Write-Output "Publishing to Azure Repo" $patUser = $env:BHAzureBuildUser $patToken = $env:BHAzureBuildPassword $securePat = ConvertTo-SecureString -String $patToken -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential($patUser, $securePat) Publish-Module -Path $env:BHModulePath -Repository $env:BHPublishRepo -Credential $credential -Verbose #Do I need a NuGetAPIKey parameter here? Write-Output "`nFINISHED: Deployed to Azure." } Task 'PublishPSGallery' -Depends 'BuildNuget' { #$lines Write-Output "Publishing to PowerShell Gallery" Publish-Module -Path $env:BHModulePath -Repository 'PSGallery' -NuGetApiKey $env:BHPSGalleryKey #Do I need a NuGetAPIKey parameter here? Write-Output "`nFINISHED: Deployed to PSGallery." } Task 'Sign' { #$Lines Write-Output "Checking for Self-Signed Certs and Signing Your Code`n" if ($PSVersionTable.PSEdition -eq "Desktop") { $OSVer = Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object Version if ($OSVer.Version.StartsWith(10)) { $ExistingCerts = Get-ChildItem -Path Cert:\CurrentUser\Root -Recurse -CodeSigningCert if (-not ($ExistingCerts)) { # specific to Win 10 and Server 2019 and later Write-Output "Creating a new self-signed Cert and adding it into your Personal Trusted Root`nWindows will prompt you to accept the new certifcate" $cert = New-SelfSignedCertificate -Type CodeSigningCert -Subject $env:BHProjectName -CertStorelocation Cert:\CurrentUser\My Move-Item -Path $cert.PSPath -Destination "Cert:\CurrentUser\Root" $ExistingCerts = Get-ChildItem -Path Cert:\CurrentUser\Root -CodeSigningCert } $publicFunctions = @( Get-ChildItem -Path "$env:BHModulePath\Public\*.ps1" -Recurse -ErrorAction 'SilentlyContinue' ) foreach ($function in $publicFunctions) { Set-AuthenticodeSignature -FilePath $function -Certificate $ExistingCerts[0] } } } elseif ($PSVersionTable.PSEdition -eq "Core") { if (($isMACOS) -or ($isLinux)) { "`n" Write-Output "As of August 2019, there is no PowerShell module for PS Core on OSX/Linux that will let you sign a script" <# Write-Output "You are going to need to enter a password for your pfx file" # using this article as a reference - http://thecuriousgeek.org/2014/02/creating-openssl-code-signing-certs-on-windows/ # and this one for installing a signing cert on a Mac - https://wilsonmar.github.io/powershell-on-mac/ Exec {openssl genrsa -out "$env:BHModulePath/Certs/ca.key" 2048} Exec {openssl req -config $env:BHModulePath/Certs/openssl.cfg -new -x509 -days 1826 -key $env:BHModulePath/Certs/ca.key -out $env:BHModulePath/Certs/ca.crt} Exec {openssl genrsa -out $env:BHModulePath/Certs/codesign.key 2048} ## Can I delete this line? Exec {openssl req -config $env:BHModulePath/Certs/openssl.cfg -new -key $env:BHModulePath/Certs/codesign.key -reqexts v3_req -out $env:BHModulePath/Certs/codesign.csr} Exec {openssl x509 -req -days 1826 -in $env:BHModulePath/Certs/codesign.csr -CA $env:BHModulePath/Certs/ca.crt -CAkey $env:BHModulePath/Certs/ca.key -extfile $env:BHModulePath/Certs/openssl.cfg -set_serial 01 -out $env:BHModulePath/Certs/codesign.crt} Exec {certtool i $env:BHModulePath/Certs/ca.crt} Exec {openssl pkcs12 -export -out $env:BHModulePath/Certs/codesign.pfx -inkey $env:BHModulePath/Certs/codesign.key -in $env:BHModulePath/Certs/codesign.crt} $MyCertFromPfx = Get-PfxCertificate -FilePath $env:BHModulePath/Certs/codesign.pfx $publicFunctions = @( Get-ChildItem -Path "$env:BHModulePath\Public\*.ps1" -Recurse -ErrorAction 'SilentlyContinue' ) foreach($function in $publicFunctions){ Set-AuthenticodeSignature -FilePath $function -Certificate $MyCertFromPfx } #> } else { # If this is PS Core Running on Windows, what version is it? If Win10, great, here's something to try. if ($OSVer.Version.StartsWith(10)) { $ExistingCerts = Get-ChildItem -Path cert:\LocalMachine\My -Recurse -CodeSigningCert if (-not ($ExistingCerts)) { # specific to Win 10 and Server 2019 and later New-SelfSignedCertificate -Type CodeSigningCert -Subject $env:BHProjectName | Out-Null $ExistingCerts = Get-ChildItem -Path Cert:\LocalMachine\My -CodeSigningCert } $publicFunctions = @( Get-ChildItem -Path $($env:BHModulePath + $env:BHPathDivider + "Public" + $env:BHPathDivider + "*.ps1") -Recurse -ErrorAction 'SilentlyContinue' ) foreach ($function in $publicFunctions) { Set-AuthenticodeSignature -FilePath $function -Certificate $ExistingCerts[0] } } # If not win10, soorrrryyyyy else { Write-Output "Sorry, no soup for you - no cert options configured" } } } } |