CI/CD pipeline for a PowerShell module in Jenkins with psake and PSDeploy

Today I would like to present how I use a CI/CD pipeline in Jenkins for one of my PowerShell modules. Along with Jenkins, I am also using psake and PSDeploy in the examples below.

There are two ways of configuring a pipeline in Jenkins - manually in GUI and using a Jenkinsfile. But, anyway, after playing with GUI (which is actually good when you are just getting to know Jenkins) at some point you really need to start with keeping you configuration in a file and hence in a VCS (Git).

Pipeline

In Jenkins, the language of a configuration file (which is called Jenkinsfile) is Groovy, however you don’t have to learn it because there is an embedded assistant in Jenkins which can convert a piece of code or tasks you need to do to Groovy language.

Let’s have look at a pipeline of one of my PowerShell modules:

As you can see, the flow goes through stages one by one, there is no any parallel work here and also there is something unusual (red circle) at ‘Deploy to PROD’ stage which we will get back to later on.

Now, I am going to describe every stage of the pipleine and how it might look like in the Jenkinsfile.

Analyze

stage('Analyze'){
    node {
        checkout scm
        stash 'everything'
        powershell '.\\build.ps1 -Task \'Analyze\''
    }
}

In PowerShell, I would say that it has become a best practice to always use PSScriptAnalyzer, therefore before you even start thinking about further steps you usually ensure PSScriptAnalyzer approves your job.
So what happening in the Groovy piece of code above is:

  • Declare stage with name Analyze
  • Declare a new node block. You can think of node as a unit of measurement, and, for instance, you can execute multiple node blocks in parallel (but this is out of scope of this article).
  • Get code from SCM. Please note that before doing this you would need to set it up in Jenkins GUI.
  • Stash (‘save’) the code under code name everything. It will allow accessing the saved code from different nodes and stages.
  • And, finally, launch build.ps1 with Analyze argument.

build.ps1:

[cmdletbinding()]
param([string[]]$Task = 'default')
$ErrorActionPreference = 'Stop'
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
if (!(Get-Module -Name Pester -ListAvailable)) { Install-Module -Name Pester -Scope CurrentUser }
if (!(Get-Module -Name psake -ListAvailable)) { Install-Module -Name psake -Scope CurrentUser }
if (!(Get-Module -Name PSDeploy -ListAvailable)) { Install-Module -Name PSDeploy -Scope CurrentUser }
Invoke-psake -buildFile "$PSScriptRoot\psakeBuild.ps1" -taskList $Task -Verbose:$VerbosePreference
if ($psake.build_success -eq $false) {exit 1 } else { exit 0 }

Since I use psake for build automation, basically, I am just launching Analyze task in psakeBuild.ps1 file:

task Analyze {
    $saResults = Invoke-ScriptAnalyzer -Path $PSScriptRoot -Recurse -Verbose:$false
    if ($saResults) {
        $saResults | Format-Table
        Write-Error -Message 'One or more Script Analyzer errors/warnings were found. Build cannot continue!'
    }
}

Test

stage ('Test'){
    node {
        unstash 'everything'
        powershell '.\\build.ps1 -Task \'Test\''
    }
}

As you can see here, we are unstashing everything that we stashed in the Analyze stage. The reason for this is we don't know whether we are being running on the same Jenkins node or not. Other nodes would not have access to the code we checked out at Analyze stage, but since we stashed it, now we can unstash and get access to the code.
Then we are just launching build.ps1 script with Test parameter. Here is an example of the Test task in my psakeBuild.ps1 file:

task Test {
    $testResults = Invoke-Pester -Path $PSScriptRoot -PassThru -OutputFile $testResultsFileName
    if ($testResults.FailedCount -gt 0) {
        $testResults | Format-List
        Write-Error -Message 'One or more Pester tests failed. Build cannot continue!'
    }
}

Publish

stage ('Publish') {
    node {
        unstash 'everything'
        nunit testResultsPattern: '*.xml'
    }
}

Here we are just publishing our test results in NUnit format generated at the Test stage.

Deploy

If all tests passed we can proceed with deployment:

stage ('Deploy to DEV') {
    node {
        unstash 'everything'
        powershell '.\\build.ps1 -Task \'DeployToDev\''
    }
}

stage ('Deploy to PROD') {
    input ('Deploy to Production?')
    node {
        unstash 'everything'
        powershell '.\\build.ps1 -Task \'DeployToProd\''
    }
}

You can notice here we are using ‘input’ command which is basically a MessageBox waiting for a user to click 'Yes' or 'No'. And if you click 'Yes' the code continues, otherwise – it stops and results in red circle in the picture shown at the beginning of the article.

psakeBuild.ps1 part for deployment:

task DeployToDev {
    Invoke-PSDeploy -Path $psDeployScript -Verbose:$VerbosePreference -Tags Dev -Force
}

task DeployToProd {
    Invoke-PSDeploy -Path $psDeployScript -Verbose:$VerbosePreference -Tags Prod -Force
}

And for deployment I am using here PSDeploy which might look like this:

Deploy "Deploy to $DevLocation" {  
    By FileSystem  {
        FromSource '.'
        To $DevLocation
        Tagged Dev
        WithOptions @{
            Mirror = $true
        }
    }
}
Deploy "Deploy to $ProdLocation" {  
    By FileSystem  {
        FromSource '.'
        To $ProdLocation
        Tagged Prod
        WithOptions @{
            Mirror = $true
        }
    }
}

Conclusion

To make it fully automated, one more step needs to be done - you need set up Jenkins to poll Git by some event/schedule/trigger. In this case you can use the pipeline mentioned above to get your code automatically analyzed, tested and deployed.

All files are available in my GitHub repo.

Comments

comments powered by Disqus