Recently I have leaned how to set up a CI pipeline in Azure DevOps for my PowerShell binary module development project. I thought may be there are others who may benefit from the information I gathered.
If you have written a PowerShell binary modules with C#, you probably know it is somewhat cumbersome to write codes, build and test the new module.
Using Azure DevOps, you can automate most of the steps involved in module development. Once it is all set up, all you would need to test the module is to install the new package (from the PS repository set up in Azure Artifacts) and import it.
When I was working on this project, I referenced many blogs, articles and YouTube videos. Though I find them very helpful, I also found that many of them sort of assumed you know many things already. I have never used Azure DevOps or other DevOps tools before writing this post. So in this guide I try not to assume your knowledge level as much as possible. If find that you need certain knowledge, I will try to document that in my guide. So hopefully this article can be your one time stop for creating your first Azure DepOps CI pipeline for your PowerShell binary module!
Prerequisites:
You have have some familiarity on how to create PowerShell binary modules. Also be familiar with steps in creating VS project for PowerShell binary module. I will be using steps described in this article. In this blog post, I will be using Visual Studio to manage local repository. If you do not have Visual Studio installed, please install it.
You have to know at lease a little bit about Azure DevOps. If you have no idea what Azure DevOps is or does, please read up on it on Google or watch tutorials on YouTube. You also need to have an Azure DevOps account to start using it.
You have to know what Git is and how to use it. At least you should know what "git clone", "git add", "git commit" and "git push" do.
You have to have some understanding of NuGet. If you have no idea what NuGet is or does, please watch those videos on YouTube.
You also need to be familiar with how PowerShell repository works. We will be using Azure Artifacts as PS repository. If you have not already, please read this article from Microsoft and learn how to use Azure Artifacts as your personal PS repository.
You also need to know how to write Pester test scripts and how they work.
PART ONE
Setting up your Azure DevOps Project
1. Please log into your Azure DevOps account and go to your organization. (Please create one if you have not already).
2. Click on New Project. Enter the project name and description. Select Private for the Visibility setting. My recommendation is to name the project same as your Module name. Click on Create.
3. In your newly created Project, locate Repos on the left. Click on Repos.
4. In Clone to your computer, select Clone in Visual Studio and click on Clone in Visual Studio. When prompted, click on Open Visual Studio.


5. Note that Visual Studio automatically creates a local folder with the name same as your project. Click on Clone.
6. Congratulations! At this pointed you have created a local repository for your Azure DevOps project. Is is still an empty repository.
PART TWO
Create Your PowerShell Binary Module on your local repository.
1. In Visual Studio, locate the Solution Explorer. Right click on your Project and select Open Folder in Explorer.
2. Notice that in the directory there are no files other than .vs and .git folders. This is still an empty repository.
3. But first you need to install PowerShell Standard Module Template. Open the repo directory in PowerShell and enter below at the command prompt.
dotnet new -i Microsoft.PowerShell.Standard.Module.Template
4. Next, you will generate source files necessary for creating the PowerShell modules. At the command prompt, type below and hit enter.
5. Now you should see the sample.cs file and csproj file with your project name.
6. Now that you have the source files created, you need to "push" them to the Azure Repos. From Visual Studio go to View, Git Change. This will launch Git window if not already. Enter the commit message and click on Commit All. Then click on the Push button to push the change to Azure Repos.
7. Go back to your browser and check your Repos. You should see the source files you just created.
PART THREESetting up your Feed in your Azure Artifacts.
1. In this section, will be setting up your feed in your Azure Artifacts. The steps are already described in
this article that I mentioned earlier.
2. In your Azure DevOps project, locate Artifacts on the left and click it. Click on Create Feed.
3. Enter the name of your feed, leave all the options as default and click Create.
4. Click on Connect to feed. Click on DotNet.
5. Copy the feed uri as shown below.
6. From this point on, we will be writing PowerShell script to add the Azure Artifacts feed as your personal PS repository. So, open PowerShell editor of your choice and save the feed URI as below as a variable. Note that you need to use the V2 version of URI not the V3 version shown in the Azure Artifacts feed property. All you need to do is just truncate the V3 uri by removing "v3/index.json" and replace with "v2".
7. Next you need to retrieve your Personal Access Token. Go back to the browser and locate User Setting on the upper right corner right next to your user icon. Select Personal Access Tokens.
8. Click on New Token, enter the name of the token, select Full access in Scopes. Click Create.
9. Copy the token.
10. Go back to your PoweShell editor and save the token as variable.
11. Finally your script should look like below. You need to save your Azure DevOps login name in $AzureDevOpsUserName.
Import-Module PowerShellGet
#PAT Token for the Organization.
$PATToken = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | ConvertTo-SecureString -AsPlainText -Force
$AzureDevOpsUserName = "username@domain.com"
$credsAzureDevopsServices = New-Object System.Management.Automation.PSCredential($AzureDevOpsUserName, $PATToken)
$FeedUri = "https://pkgs.dev.azure.com/Modules/Module/_packaging/Feed/nuget/v2"
$Params = @{
'Name' = 'AzureDevOpsArtifact'
'InstallationPolicy' = 'Trusted'
'SourceLocation' = $FeedUri
'PublishLocation' = $FeedUri
}
Register-PSRepository @Params -Credential $credsAzureDevopsServices
12. Open PowerShell and run this script. You will be prompted to enter code and password for one time machine authentication.
13. You should be able to see your PS repository by running:
Get-PSRepository -name AzureDevOpsArtifact | fl *
14. In order to see the packages in this repository you can issue Find-Module command, but you need to give your credential.
$PATToken = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | ConvertTo-SecureString -AsPlainText -Force
$AzureDevOpsUserName = "username@domain.com"
$credsAzureDevopsServices = New-Object System.Management.Automation.PSCredential($AzureDevOpsUserName, $PATToken)
Find-Module -Repository AzureDevOpsArtifact -Credential $credsAzureDevopsServices
15. At this point you should not find any module in the PS Repository as the feed is still empty. Congratulations! you now have set up your Azure Artifacts feed as your personal PowerShell repository.
PART FOUR
Preparing Module Manifest file and nuspec file.
In order for PowerShell to be able to import your module with Import-Module command, there needs to be a folder with the same name as your module name and it has to exists somewhere in your $env:PSModulePath. Additionally in the module folder, there needs to be file with the same name as your module with file extension of .psd1 (PS Manifesto file). In the case of binary module, in the psd1 file, you need to specify the name of the binary module (the name of the dll file) specified in NestedModules property.
Another file required in the module build process is nuspec file. In the CI pipeline we will use NuGet to package the module. NuGet will use the nuspec file to retrieve necessary information in packaging.
Both psd1 file and nuspec file can be built from csproj file for your project.
1. Open PowerShell Editor of your choice and paste below codes. This is just a sample code so you do not need to use this sample. But you get the idea.
There are few things to note here. The Module name and the the namd of the dll file you specify in -NestedModules @("$ModuleName.dll") has to match. If you name the Azure DevOps project same as your Module name, then you do not need to change anything here.
The psd1 file has to be placed in the Release directory. This is the directory the build process will place the binary module files and dependency files.
Also in creating spec file, you need to specify PSModules in <tags>PSModule</tags>. The nuspec file should be created in the $env:System_DefaultWorkingDirectory.
# Variables
$ModuleName = "MyTestModule"
$DefaultWorkingDirectory = $env:System_DefaultWorkingDirectory
$ArtifactStagingDirectory = $env:Build_ArtifactStagingDirectory
$ReleaseDir = "$ArtifactStagingDirectory\bin\Release\netstandard2.0"
$CSprojFilePath = "$DefaultWorkingDirectory\$ModuleName.csproj"
#Generate PSD1 file from csproj file.
[xml]$Project = Get-Content -Path $CSprojFilePath
$Version = $Project.Project.PropertyGroup.Version
$Authors = "Your Name"
$Company = "Your Company Name"
New-ModuleManifest -Path "$ReleaseDir\$ModuleName.psd1" `
-ModuleVersion $Version `
-Author $Authors `
-CompanyName $Company `
-Copyright "(c) 2021 $Company All rights reserved." `
-Description 'PowerShell Module Test' `
-NestedModules @("$ModuleName.dll") `
-CmdletsToExport @("*")
$spec =@"
<?xml version="1.0" encoding="utf-8"?>
<package >
<metadata>
<id>$ModuleName </id>
<version>$Version</version>
<authors>$Authors</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<projectUrl>http://project_url_here_or_delete_this_line/</projectUrl>
<iconUrl>http://icon_url_here_or_delete_this_line/</iconUrl>
<description>Package description</description>
<releaseNotes>Summary of changes made in this release of the package.</releaseNotes>
<copyright>$copyright$</copyright>
<tags>PSModule</tags>
<dependencies>
<group targetFramework=".NETStandard2.1">
</group>
</dependencies>
</metadata>
</package>
"@
Set-Content -Value $spec -Path "$DefaultWorkingDirectory\$ModuleName.nuspec"2. Save this script in your local repository directory that you created in the Part One. Name this script like Build.ps1.
3. At this point you also need to edit the csproj file so it has the version number and also add below line in <PropertyGroup></PropertyGroup> element if your module has dependency on other libraries. If your csproj file does not show the version number, right click your project in the solution explorer of Visual Studio, select properties. In the project properties, select Package and update the version number. Save the change.
<PropertyGroup>
<Version>1.0.0</Version>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
4. Commit the change and push the change to the Azure Repos. Now your Repos should look like this.
PART FIVE
Putting all together in Azure Pipeline
1. Now let's go back to the Azure DevOps console. From the left side menu, click on Pipeline.
2. In the Pipeline, click on New pipeline.
3. Click on "Use the classic editor" at "Where is your code?" prompt.
4. Select Azure Repos Git at "Select a source" prompt. Your project and repository should be already selected. Click on Continue.
5. Select .NET Desktop at "Select a template" prompt. Click "Apply".
6. You should see the pipeline created out of the template.
7. First we will remove some tasks that we don't need. Click VSTest and remove it by clicking Remove button on the right. Do the same for Publish symbols path, remove it.
8. At this point your pipeline should look like below.
10. We will be adding few more taskss to our pipeline. Click on the plus button. On "Add tasks", type "PowerShell" in the search box. When PowerShell task is displayed click on "Add".
11. Select Inline as Type and in the script box enter ".\Build.ps1", meaning this task should run the Build.ps1 in the current directory. Remember we have already pushed this Build.ps1 from our local repository to the Azure Repos in Part four.
12. Click on the plus button again to add another task. In the search box, enter "nuget". Click on "Add" to add the NuGet task.
13. On the NuGet task property, change the "Path to csproj or nuspec file(s) to pack" to **/*.nuspec so the NuGet will use the nuspec file you generated in the Build.ps1 script. Also change the "Base path" to:
$(build.artifactstagingdirectory)\bin\Release\netstandard2.0
14. Lastly we will add one more NuGet task, this time the NuGet will push the package to our Azure Artifacts feed. Click on the plus button again to add another task. In the search box, enter "nuget". Click on "Add" to add the NuGet task. In the task property, select "push" as the command and select the feed you created in Part three as your "Target feed".
15. Save and Queue the pipeline. Click on "Save and run".
16. Click on the "Agent Job" to see the status of each tasks. If all task were ran without any error, you should see the packaged module in your feed.
17. Congratulations! You have now published your PowerShell binary module in Azure Artifacts feed.
PART SIX
Installing the module in PowerShell.
1. Now that the module has been published, it is time to install it in your PowerShell. In Part Three we have registered our Azure Artifacts feed as our PSRepositry. Open PowerShell as administrator and type below.
You should see your Azure Artifacts feed.
2. Type below command to display available packages in the feed.
Find-Module -Repository AzureDevOpsArtifact
You should see the package you just built in the pipeline.
3. To install this module on your computer, you still need to provide the credential as it is a private repository. Use the same line of codes from Part Three to obtain the credential objects.
$PATToken = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | ConvertTo-SecureString -AsPlainText -Force
$AzureDevOpsUserName = "username@domain.com"
$credsAzureDevopsServices = New-Object System.Management.Automation.PSCredential($AzureDevOpsUserName, $PATToken)
4. Finally use the credential object to install the module.
Install-Module MyTestModule -Credential $credsAzureDevopsServices
5. Test your module. You should see the version number.
PART SEVEN
Continuous Integration (CI).
1. Now that we have verified our pipeline works, it is time to enable continuous integration (CI). Open browser and go back to your Azure DevOps console. Click on the Pipelines.
2. Click your pipeline and click Edit at the top.
3. Click on Triggers, check "Enable continuous integration". Now that you enabled continuous integration on this pipeline, the pipeline will run whenever there is a push to the code repository.
4. On you computer, open Visual Studio and make update on your codes for the PowerShell module.
5. In the solution explorer, right click on your project and go to the properties.
6. Update the version number.
7. Commit and push the change. The pipeline should automatically run.
8. Once the pipeline completes its run, issue Find-Module to see if the module has been updated in the repository.
PART EIGHT
Testing your module with Pester
1. Now that you have completed the CI pipeline, it is time to add testing to your pipeline. We can you use Pester to test your module.
2. Open your PowerShell editor and write a Pester test script. I wrote an example here which you can test the example module used in this post. The test will test that the version matches with your code version. It will also test the outputs of the cmdlettes are what is expected. Save this in your local repos directory as Test-Module.ps1 and push it to the Azure Repos.
Describe "Pester Test for Module" {
BeforeAll {
# Variables
$DefaultWorkingDirectory = $env:System_DefaultWorkingDirectory
$ArtifactStagingDirectory = $env:Build_ArtifactStagingDirectory
$ModuleName = "MyTestModule"
$CSprojFilePath = "$DefaultWorkingDirectory\$ModuleName.csproj"
[xml]$Project = Get-Content -Path $CSprojFilePath
$Version = $Project.Project.PropertyGroup.Version
$MajorVersion = $Version.Split(".")[0]
$MinorVersion = $Version.Split(".")[1]
$BuildVersion = $Version.Split(".")[2]
$localRepoName = "MyLocalRepository"
$LocalRepositoryPath = "$DefaultWorkingDirectory\Repository"
$PackageFileName = "$ModuleName.$Version.nupkg"
If(!(Test-Path -Path $LocalRepositoryPath)){
mkdir $LocalRepositoryPath
}
Copy-Item "$ArtifactStagingDirectory\$PackageFileName" "$LocalRepositoryPath\$PackageFileName"
$localrepository = @{
Name = $localRepoName
SourceLocation = $LocalRepositoryPath
PublishLocation = $LocalRepositoryPath
InstallationPolicy = 'Trusted'
}
Register-PSRepository @localrepository
Install-Module -Name $ModuleName -Repository $localRepoName -Force -AllowClobber
Import-Module -Name $ModuleName -Force
}
Context "Install package" {
It "Find Module Should not return any error" {
(Find-Module -Repository $localRepoName -ErrorAction Stop).Name | Should -Be $ModuleName
}
}
Context "Module Test" {
It "Check the major version" {
(Get-Module -Name $ModuleName).Version.Major | Should -Be $MajorVersion
}
It "Check the minor version" {
(Get-Module -Name $ModuleName).Version.Minor | Should -Be $MinorVersion
}
It "Check the build version" {
(Get-Module -Name $ModuleName).Version.Build | Should -Be $BuildVersion
}
It "Check the commandlet" {
$FavoriteNumber = 1
$FavoritePet = "Dog"
(Test-SampleCmdlet -FavoriteNumber $FavoriteNumber -FavoritePet $FavoritePet).FavoriteNumber | Should -be $FavoriteNumber
(Test-SampleCmdlet -FavoriteNumber $FavoriteNumber -FavoritePet $FavoritePet).FavoritePet | Should -be $FavoritePet
}
}
}
3. On your Azure DevOps console, open your pipeline. Click on the plus button to add a new task. Make sure this PowerShell task is placed right after NuGet pack task.
4. In the search box type in "PowerShell" and add a new PowerShell task.
5. In the PowerShell task property, select Inline as Type. In the Script box enter below:
Import-Module Pester -Force
Invoke-Pester -Path ./Test-Module.ps1 -OutPutFile ./Test-Pester.xml -OutPutFormat NUnitXml -Verbose
6. Click on the plus button. In the search box, type in "Test". From the list of available tasks, select "Publish Test Results" and click "Add". Make sure this task is added right after the PowerShell task you just added.
7. In the Publish Test Result property, enter "**/Test-Pester.xml" as your test results files. Also select "Even if a previous task has failed, unless the build was cancelled."
8. Finally your pipeline should look like this.
PART NINE
Viewing the test results of the Pester tests.
1. If you run the pipeline now, you should not receive any errors in the Pester tests. Try running the pipeline.
2. On your last run, you should now see Tests link.
3. Click on the Tests link and view the results of the tests.
4. Now we need to test if the Pester tests works if a bug introduced in your code. Go back to your Visual Studio. Edit the code so the Pester test actually fails. In this example you can edit C# code as below so the the fifth Pester test fails.
(Test-SampleCmdlet -FavoriteNumber $FavoriteNumber -FavoritePet $FavoritePet).FavoritePet | Should -be $FavoritePet
5. Push the change to Azure Repos, which will start the pipeline run.
6. Click on the last run, which should have failed.
7. Click on the result details. You can see the Pester error message, which shows why this test failed.