PowerShell module with continuous integration, static analysis and automatic publish to gallery

 

Recently our team released the Reporting Services PowerShell Tools as a project to collaborate with the community, we had a lot more feedback and participation that we ever thought so we decided to push it a little further and publish it to the PowerShell Gallery.

We aren't by any means PowerShell experts and when we published the module we found that we should have run the PSSScriptAnalyzer , so we run it and fixed the issues but that will not scale as a manual task also I would like to automate the publishing to the gallery so is always in sync with the code in the repo.

Investigating around I found this great starting point in the https://ramblingcookiemonster.github.io/GitHub-Pester-AppVeyor/ blog post, it describes how to use AppVeyor for running unit tests with Pester, however I wanted something slightly different, my main goals are:

  1. Provide static analysis using PSSScriptAnalyzer and show the results to the users submitting the pull request
  2. After the PR is merged into main publish automatically to the PowerShell gallery

So I started by onboarding the GitHub repo to AppVeyor and created a New Project based on our repo, the procedure is pretty straightforward and AppVeyor has plenty of documentation so I'm not including any detailed instructions.

Onboarding the ScriptAnalyzer

First thing is to add the appveyor.yml file in our GitHub repo, my initials attempts failed , basically I wanted to install the PSScriptAnalyzer module but it failed with

Exception calling "ShouldContinue" with "2" argument(s): "The method or operation is not implemented."

At C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.0.0.1\PSModule.psm1:5908 char:8

+ if($Force -or $PSCmdlet.ShouldContinue($shouldContinueQueryMessag ...

+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException

+ FullyQualifiedErrorId : NotImplementedException

Install-Module : NuGet provider is required to interact with NuGet-based repositories. Please ensure that '2.8.5.201' or newer version of NuGet provider is installed.

At line:1 char:1

+ Install-Module -Name PSScriptAnalyzer

+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ CategoryInfo : InvalidOperation: (:) [Install-Module], InvalidOperationException

+ FullyQualifiedErrorId : CouldNotInstallNuGetProvider,Install-Module

Command executed with exception: NuGet provider is required to interact with NuGet-based repositories. Please ensure that '2.8.5.201' or newer version of NuGet provider is installed.

 

The solution is to install first the nuget package provider, the install section end up being

install:
- ps: Install-PackageProvider -Name NuGet -Force
- ps: Install-Module -Name PSScriptAnalyzer -Force

 

The next step is to produce a XML results file, I spend some time figuring out what would be the simplest schema and selected junit and then I used some magic string manipulation to get the results of the script analyzer into the testresults file

# Execute the script analyzer

$results = Invoke-ScriptAnalyzer -Path .\functions -Recurse | where severity -eq "Error"

# Format the results

$header = "<testsuite tests=`"$($results.Count)`">"

$body = $results | ForEach-Object {"<testcase classname=`"analyzer`" name=`"$($_.RuleName)`"><failure type=`"$($_.ScriptName)`">$($_.Message)</failure></testcase>"}

$footer = "</testsuite>"

$header + $body +$footer | out-file .\TestsResults.xml

# Upload results

$wc = New-Object 'System.Net.WebClient'

$wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestsResults.xml))

# Fail if there are issues

if($results.Count -gt 0){throw "ScriptAnalyzer found $($results.Count) issues"}

 

That gives me this nice UI when the script analyzer finds any issue (in order to test the pipeline I removed the filter and allowed the warnings to show as test failures)

clip_image001

You can check the entire script in our repo appveyor.yml

I want to control the publishing process, I don't want every single commit to master to publish a new version of the module to the PowerShell Gallery, so I decided to use git tags in order to request a publish, basically when a new tag is added the continuous integration process will run the tests and if they pass it will use the tag label as the version to publish in the gallery, the general workflow is

  1. Update the module manifest (.psd1) with the tag version
  2. Publish to the PowerShell Gallery
  3. If the publish is successful commit back to the repo the new module manifest (.psd1)

Interacting with the PowerShell gallery is based on a secret key they provide to use it in the Publish-Module cmdlet, in order to be used in your AppVeyor script you should encrypt it, the detailed instructions are available here https://www.appveyor.com/docs/build-configuration/#secure-variables

Also here are the instructions on how to interact with git from the AppVeyor script https://www.appveyor.com/docs/how-to/git-push/

One issue that I hit was that my git commands from powershell were treating the git console messages as errors, searching around found this workaround and implemented in the AppVeyor script, the deploy section looks like this.

 

# Deploy to Powershell Gallery only where there is a tag which will have the version number

$deploy = ($env:APPVEYOR_REPO_TAG -eq $true)

if ($deploy)

{

   git checkout master

   Write-Host "Starting Deployment tag $env:APPVEYOR_REPO_TAG_NAME"

   $moduleName = "ReportingServicesTools"

   $currentVersion = (Import-PowerShellDataFile .\$moduleName.psd1).ModuleVersion

   ((Get-Content .\ReportingServicesTools.psd1).replace("ModuleVersion = '$($currentVersion)'", "ModuleVersion = '$($env:APPVEYOR_REPO_TAG_NAME)'")) | Set-Content .\$moduleName.psd1

   Publish-Module -Path .\ -NuGetApiKey $env:galleryPublishingKey

   git config --global core.safecrlf false

   git config --global credential.helper store

   Add-Content "$env:USERPROFILE\.git-credentials" "https://$($env:access_token):x-oauth-basic@github.com`n"

   git config --global user.email "yourEmail@outlook.com"

   git config --global user.name "Your Name"

   git add ReportingServicesTools.psd1

   git commit -m "Automatic Version Update from CI"

   git push

}

 

For some unexplainable reason I wasn’t able to use the Update-ModuleManifest cmdlet so I did the manual replacing of the module version.

If you are interested in the entire deployment file it is available here in our repo appveyor.yml

Something I realized after I finished is that GitHub handle the tags as releases, which is perfect for my intentions, because now when we need to publish a new version we just create a release in GitHub and it will show up automatically as a release in the PowerShell gallery

Release in GitHub and in the PowerShell gallery

imageimage

Finally many thanks to a fellow Microsoft engineer Sergei Vorobev who provided various tips around the entire process.