Written by Taylor Gibb, Developer with Derivco.
I have been doing a lot of PowerShell work over the past six month for a big internal project. This encouraged me to do some open source PowerShell work, and ultimately resulted in me setting up a CI pipeline for my open source PowerShell modules. The pipeline is responsible for:
- Linting
- Running tests
- Conditional Deployment
Linting is easily accomplished with PSScriptAnalyzer and tests with Pester. Deploying PowerShell modules to the PowerShell Gallery on the other hand is admin, and i dont like admin. So the idea was that every time a commit on the master branch contained [deploy]
in the commit message, we would kick off a deployment task that updated the modules.
Repository Structure
For the simplicity of this article, i am going to assume your repository is set up as follows.

You can probably edit my build script to work for other configurations, but some values, like the path in which to look for scripts to be included in the code coverage results are hard-coded to the layout of my repository. Nevertheless, you are probably wondering what some of those files are, and rightly so. In this article we will look at creating:
.gitlab-ci.yml
– the Gitlab CI filebuild.ps1
– the file that is executed by the GitLab runner*.PSDeploy.ps1
– a file used by PSDeploy to publish our module to the PowerShell Gallery*.Tests.ps1
– unit tests which are run by Pester
Gitlab CI
I am a big fan of the Gitlab CI solution. I have a build runner sitting in a VM on Azure and it is very easy to set up. You can check out the instructions on how to set it up over on their website, but be sure to choose PowerShell as the executor during the installation. Once the runner is set up, create a .gitlab-ci.yml
in the root of your repository and populate it as follows.
This tells the build runner to call a file called build.ps1
in the same directory, with a list of tasks. Its important to notice that the release
task is only run on the master branch, while all other branches only run the analyze
and test
tasks. So lets take a look at what build.ps1
looks like:
Pester
So far we have a build that doesn’t do much except do some lint checking via PSScriptAnalyzer, so lets take it one step further and add a test. Since i already had the code, i just added a test to my Test-LevenshteinDistance
function, which as you can imagine, tests the Levenshtein Distance between two strings. All i needed to do was add a file along side the script, and append .Tests.ps1
to the name of the script. This left me with the Test-LevenshteinDistance.ps1
and Test-LevenshteinDistance.Tests.ps1
files you see in the repository layout screenshot. Below you can see what my Test-LevenshteinDistance.Tests.ps1
file looks like, notice that Pester has its own DSL, but there is plenty documentation on the Pester wiki to help you write your tests.

You should now be able to run the build script on your local machine and get some test coverage. If for some reason your tests fail, it will list the failed tests as well. In our case, all is looking good.

We also need to add a regex so that Gitlab can pick up our test coverage and report it using one of those labels that all the cool kids are using. So head into the Gitlab web interface, open your project and choose to edit the CI\CD Pipelines. You will need to enter the following regex.
Code Coverage: (\d+.\d+%)

Now when a build, runs you will be able to see your test coverage in the build report.

PSDeploy
We will also need to add a PSDeploy file. I just added one for my algorithms module. There are a lot of deployment targets, but i needed the PowerShell Gallery. If you would like to publish to something like the file system, be sure to check our their documentation. Here is the one i put together, once again you can consult the repository layout to see exactly where it goes.

You will notice that i am using an environment variable, we dont want to commit our api key to source control after all. This does however mean i need to configure it in the Gitlab web interface, but there is no magic here. We just need to be sure to name it NugetApiKey
.

That is pretty much all there is to it.

Next time we look at how to add custom Script Analyzer rules, or override the default ones. As always, i welcome feedback in the comments!