A GitLab CI/CD pipeline for PowerShell scripts
Using GitLab CI/CD to automate the publishing of PowerShellGet script packages to multiple release channels.
Published in Articles on by Michiel van Oosterhout ~ 5 min read
This article demonstrates an implementation of the Git-based workflow described in an earlier article. The workflow, implemented using a GitLab CI/CD pipeline, will publish a PowerShell script package to a prerelease and a release channel. We will use the same script described previously.

Git repository layout
The code for the script package will be hosted in a repository in a dedicated GitLab project. The script file and its unit tests file should be added to the root of the repository. GitLab CI/CD pipelines are defined in a YAML file named .gitlab-ci.yml
, which must be saved in the root of the repository.
.gitlab-ci.yml
Example.ps1
Example.Tests.ps1
Publish-PowerShellScript.ps1
(The Publish-PowerShellScript.ps1
file is described in detail in a previous article.)
GitLab CI/CD pipeline
Docker image
GitLab uses the GitLab Runner application to run pipelines. This application implements a number of executors that execute a pipeline's jobs in an environment, for example in a Virtual Machine or in a Docker container. By default GitLab's hosted runners will execute jobs in a Docker container based on the ruby:2.5
image. Since our script needs PowerShell, we set the default image for al jobs to Microsoft's official PowerShell image.
default:
image:
name: mcr.microsoft.com/powershell:7.2-ubuntu-22.04
You could create a custom Docker image that contains all the software required by the script, and set that as the default, but this outside of the scope of this article.
Define stages
By default a GitLab CI/CD pipeline has 3 stages: build, test, and deploy. We override that by defining our own set of stages:
stages:
- publish
Publish
GitLab CI/CD pipelines support YAML anchors as a way to compose complex pipelines out of re-usable parts. We define a hidden job, which we will then re-use in each pipeline via anchors. The job selects the stage to run in using the stage
keyword.
.publish: &publish
stage: publish
script:
- apt-get update && apt-get install -y git dotnet6
- |
pwsh -c '
Set-PackageSource PSGallery -Trusted > $null
Install-Module Pester
Install-Module PSScriptAnalyzer'
- |
pwsh -c '
. ./Publish-PowerShellScript.ps1
Publish-PowerShellScript `
-Ref ($Env:CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ?? $Env:CI_COMMIT_BRANCH ?? $Env:CI_COMMIT_TAG) `
-Build $Env:CI_PIPELINE_IID `
-NuGetApiKey "$Env:NUGET_API_KEY" `
-NuGetUrl "$Env:NUGET_URL"'
The Docker image we use is missing a few required software packages and PowerShell modules. The first 2 script
entries install these requirements. Because our image is Ubuntu-based we can use apt-get
to install Git and the .NET CLI. Notice that we need to call pwsh
to run PowerShell commands, since the Docker image we use uses Bash as its default shell.
The value of the -Ref
parameter uses a predefined CI/CD variable to pass the value of the current version tag, or the value of the main branch, or the value of the merge request branch. This will be used by the script to determine the correct versioning strategy.
The value of the -Build
parameter is an incrementing number that uniquely identifies the run in this GitLab project, and will be used by the script to generate an incrementing prerelease label for each iteration of a merge request.
The values for both -NuGet
parameters come from CI/CD variables scoped to an environment. This way the script does not need to know about any specific release channel.
Failed tests report
GitLab CI/CD has built-in support for reporting failed unit tests, which requires test data in the JUnit format.
after_script:
- |
pwsh -c '
New-Item test-results -Force -Type Directory > $null
$transform = [System.Xml.Xsl.XslCompiledTransform]::new();
$transform.Load("https://raw.githubusercontent.com/jenkinsci/nunit-plugin/ec8e9079/src/main/resources/hudson/plugins/nunit/nunit-to-junit.xsl");
$transform.Transform("./artifacts/tests.xml", "./artifacts/junit.xml")'
artifacts:
reports:
junit: artifacts/junit.xml
The after-script
, which runs even if the script
before it failed, transforms the NUnit test data to JUnit format. By declaring this file a report artifact, GitLab CI/CD will automatically display a test report for each pipeline.

Triggers
There should be 3 pipelines, one for each scenario: main
for when commits are pushed to the main branch, merge
for when a merge request is (re)opened or its head branch is updated, and tag
for when a semantic version tag (e.g. v1.2.3
) is created.
main:
<<: *publish
environment: MyGet
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
merge:
<<: *publish
environment: MyGet
rules:
- if: $CI_MERGE_REQUEST_ID
tag:
<<: *publish
environment: PowerShell Gallery
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
Similar to Bitbucket Pipelines, GitLab CI/CD requires us to declare a separate pipeline for each scenario, and allows the use of YAML anchors to re-use the publish job declared earlier. What is different is that here, instead of specifying triggers, we need to use rules with conditional expressions to limit each pipeline to a specific scenario.
Each pipeline selects a predefined deployment environment, which is similar to environments in GitHub and Bitbucket repositories. Each environment can have (masked) variables associated with it, which become available to jobs as environment variables. This is how we can use the environments to represent release channels.

Summary
Pipelines in GitLab CI/CD can be used to automate the publishing of PowerShell Get script packages to multiple release channels. YAML anchors allow re-use and composition of jobs. By targeting a specific environment, each pipeline ensures that the script remains unaware of release channels. Instead of specific triggers, conditional expression are used to limit each pipeline to run only for a specific scenario.