One of the first step I took in the PowerShell journey of my team, and the topic of this post, was to address the need for a Continuous Integration and Delivery (CI/CD) pipeline for our PowerShell modules. Since the modules are built, published and consumed by our internal company infrastructure, there are some nuances that I had to account for when designing our approach for the pipeline. Having a well-defined, rock-solid CI/CD pipeline becomes a must-have as PowerShell develops into a key tool in our automations and processes.

Currently, the PowerShell pipeline has the following goals:

• Install the module dependencies
• Validate PowerShell code syntax
• Run static code checker
• Run tests in order of complexity: unit tests, then component tests, etc.
• Package the module to be published

## Before we begin #

Some of the concepts and techniques that we are going to cover on this post are not necessarily at a beginner's level; it is intended for those already familiar with PowerShell, instead.
The implementation that we discuss below, builds mainly on existing work and knowledge shared by Kevin Marquette, Warren Frame and Mark Kraus. If you need to cover some ground on PowerShell modules and pipelines, you should checkout first the following posts:

You're back! Awesome, let's first cover some of the dependencies that we use to build the pipeline with.

## Modules in use #

To run the pipeline, we use several modules from the community, as well as some developed internally, that facilitate achieving the goals outlined for our system earlier in this post.

### Community modules #

As I mentioned them in a previous post about PowerShell, these modules are among the building blocks for any serious endeavors in PowerShell, well at least for me :).

Besides these excellent PowerShell modules, we use an internal module to generate a .nupkg file for the current module in the pipeline, so it can be stored to Artifactory, which act as our internal PowerShell gallery.

## Pipeline #

The pipeline is composed of two main components: a PowerShell implementation, that handles everything from validating the code, running tests and packaging the module; and the Jenkins component, that oversees the whole CI/CD starting from Git, passing through the PowerShell pipeline and ending it with the deployment to Artifactory.

This separation was drawn by our infrastructure itself, which only allows to publish to Artifactory repositories from our internal Jenkins servers. Enforcing this separation allows contributors to run the majority of the pipeline on their local boxes, thus improving the feedback cycle, quality and productivity.

### PowerShell #

The PowerShell component of the pipeline handles the following tasks:

• Run static analysis, which includes tokenization to detect errors in PowerShell files using PSParser, verification of compliance with PSScriptAnalyzer rules (run as Pester tests) and presence of Set-StrictMode -Version Latest at the top of the files.
• Build the Module (which includes copying all relevant files to the Output folder and update the module manifest (.psd1 file) accordingly.
• Run unit tests and integration tests with Pester to verify the business logic of the module.
• Publish the module as a .nupkg file in the Output location.

#### build.ps1 #

The build.ps1 file is the entry point for the PowerShell automation. As part of its execution, it:

• Makes sure that NuGet packaged provider is configured (a prerequisite) and that module dependencies specified in the Development.depend.psd1 are locally met by using PSDepend.
• Sets the environment information, i.e. detecting whether is running on a CI tool (e.g. Jenkins, Appveyor), a git repo is reachable in the path, etc., using BuildHelpers.
• Finally, triggers the execution of the task-based portion of the automation, defined using InvokeBuild.

The implementation is heavily based on the build.ps1 from https://github.com/KevinMarquette/PSGraphPlus

<#.DescriptionInstalls and loads all the required modules for the build.Derived from the PSGraphPlus repo (https://github.com/KevinMarquette/PSGraphPlus)#>[CmdletBinding()]param (    [Parameter()]    [ValidateNotNullOrEmpty()]    [string]    $Task = 'Default', [Parameter()] [ValidateNotNullOrEmpty()] [string]$RepositoryName = 'TempFeed',    [Parameter()]    [ValidateNotNullOrEmpty()]    [string]    $RepositoryLocation)Set-StrictMode -Version Latestif (-not$PSBoundParameters.ContainsKey('RepositoryLocation')) {    $RepositoryLocation = "$PSScriptRoot/$RepositoryName"}Write-Output "Starting build"# Grab nuget bits, install modules, set build variables, start build.Write-Output " Install Dependent Modules for CI"try { Get-PackageProvider -Name NuGet -ForceBootstrap | Out-Null Save-Module -Name PSDepend -Path "$PSScriptRoot/Dependencies" -RequiredVersion 0.3.0 -Force -Repository Artifactory    Import-Module -Name "$PSScriptRoot/Dependencies/PSDepend" -Force Invoke-PSDepend -Path "$PSScriptRoot\Development.depend.psd1" -Install -Import -Force}catch {    Write-Error $_ exit 1}Write-Output " Import Dependent Modules"Import-Module InvokeBuild, BuildHelpersWrite-Output " Set Build Environment"Set-BuildEnvironment -Forceif (($env:BHBranchName -eq 'HEAD') -and (-not [string]::IsNullOrEmpty($env:BRANCH_NAME))) { Write-Output " Update BHBranchName envvar"$env:BHBranchName = $env:BRANCH_NAME}$params = @{    RepositoryName = $RepositoryName RepositoryLocation =$RepositoryLocation    Result = 'Result'}Write-Output "  InvokeBuild"Invoke-Build $Task @paramsif ($Result.Error){    exit 1}else{    exit 0}

#### Development.depend.psd1 #

The Development.depend.psd1 file, similar to the one shown below, lays out the required dependencies for the module during the CI process as well as common options used to describe where/how those dependencies should be installed. As options, we specified the following:

• Modules should be downloaded from our internal Artifactory repository, so we don't hit rate limits on the PSGallery for the third-party modules in use.
• Modules should be added to environment path variable, so they can be imported by name.
• Modules should be saved on a local folder at the root repo level, so they are local to the CI repo workspace and avoid polluting the powershell host with unused modules for developers and the CI nodes.

#### StaticAnalysis.Tests.ps1 #

The StaticAnalysis.Tests.ps1 file contains the tests that verifies the quality of PowerShell code written in the module. As mentioned in the beginning of this section, it takes care of:

• Tokenizing the code to verify there are no parsing errors, both for the tests and module code
• Running the allowed PSScriptAnalyzer rules, from the PSScriptAnalyzerSettings.psd1 at the module root folder, against the module source code.
• And, verifying that all files from the module and tests folder have set the strict mode to Latest, so scripts will always use the latest version available and take the advantage of the improvements of the coding rules verification.

## Future Improvements #

As always in software, there is still plenty of room for improvements. On the PowerShell side, the focus is on leveling up the security of the pipeline, by introducing detection mechanisms for security issues such as InjectionHunter, and automating the generation of Markdown documentation, based on the comment-based help for the cmdlets using PlatyPS. From an infrastructure perspective, we want to start leveraging docker and containers to build and release the modules to reduce the dependency on specific VM maintained for PowerShell modules in our build infrastructure.

## Conclusions #

Thank you so much for reading this post. It was longer than I originally expected, but I hope that anyways you liked reading it as much as I did writing it. Stay tuned for more!!

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer’s view in any way.