How to publish and use PowerShell modules with Azure DevOps
Tags: azure, devops
Published: 2022-09-20 21:20:12

If you write PowerShell modules you may have faced this problem; You want to share these modules privately with all your company users and internal pipeline, but you don't grant access to external users. 
Public gallery, like PowerShell gallery, is no longer an option. What can you use?

One of the responses is Azure DevOps. Azure DevOps not only has, git repositories, pipelines, and boards, but it also has an artefact manager, Azure Artefacts

There are several steps to use Azure Artefacts as a PowerShell modules repository. 

First, you need an Azure DevOps organization and a project (go to https://dev.azure.com/).
On the left menu, select “Artifacts” and create a feed by clicking on the “Create Feed” button. 
Give a name to the fee, ie ps-private-module and leave the default options, then click on Create.  
You will also need to add the default build service as a contributor to the feed. Go to the feed settings (the little gear on the right) and then permission and add the build service as a contributor (its name should be your-project-name Build Service) 

Now we can work on the module. 
We need to create a module manifest 
`New-ModuleManifest -Path  ./demomodule.psd1 -RootModule demomodule.psm1`

We will need to put the two files into a demomodule folder in an Azure DevOps repository.

The next step is to package the module so it can be put in a PowerShell repository, public or private. 
For that we use NuGet. NuGet (https://www.nuget.org/) is the .NET package manager used to share code and compiled code. 

Open a shell and move to the demomodule folder then type 

`Nuget spec demomodule`

It will create a NuGet package specification file, demomodule.nuspec in the folder. The nuspec file is an XML file. 

```
<?xml version="1.0"?>
<package >
  <metadata>
    <id>demomodule</id>
    <version>1.0.0</version>
    <authors>olivi</authors>
    <owners>olivi</owners>
    <licenseUrl>http://LICENSE_URL_HERE_OR_DELETE_THIS_LINE</licenseUrl>
    <projectUrl>http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE</projectUrl>
    <iconUrl>http://ICON_URL_HERE_OR_DELETE_THIS_LINE</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Package description</description>
    <releaseNotes>Summary of changes made in this release of the package.</releaseNotes>
    <copyright>Copyright 2022</copyright>
    <tags>Tag1 Tag2</tags>
    <dependencies>
      <dependency id="SampleDependency" version="1.0" />
    </dependencies>
  </metadata>
</package>
```

You will need to change the URI of the license (your Azure Repos URI), and project (Azure board URI), you can remove the iconUrl (except if you have one) and remove the <dependency id="SampleDependency" version="1.0" /> line and also the tag

The most important thing to take into account is the version, the version should be the same as what you have in the PowerShell module manifest. 

The next step is to create the package and send it to Azure DevOps Artefact.

Let’s create a build folder and put a build.ps1 file. The purpose of this file will be to package the module as a nugget package and send it to Azure DevOps Artefacts.

Packaging the module as a nugget package is done by using the NuGet pack command in the module folder 
`Nuget pack demomodule.nuspec `

This will create a nupkg file. Then will need to send this nupkg file to Artefact. For that NuGet source add and NuGet push will be used.
The first create a NuGet source in the local configuration using the NuGet API of Azure Artefact. 
But we will need a username and password. We can use a Personal Access Token (PAT) but as we want to run the build task in an Azure DevOps pipeline we can use the system_accesstoken.

```powershell
push-location -Path ./demomodule

& nuget pack ./demomodule.nuspec  

$accessToken = $env:SYSTEM_ACCESSTOKEN 

[xml]$nugetFileData = Get-Content -Path  ./demomodule.nuspec  

$moduleVersion = $nugetFileData.package.metadata.version 

nuget source add -Name "AZDevOpsPWSHModule" -Source "https://pkgs.dev.azure.com/AZ-OMDEMO/demo-psmodule-artifact/_packaging/ps-private-module/nuget/v3/index.json" -username $accessToken  -password $accessToken 

nuget push -Source "AZDevOpsPWSHModule" -ApiKey AzureDevOpsService  "./demomodule.$($moduleVersion).nupkg" 
```

The first line set the location in the module folder. The second line package the module using the NuGet pack command. This will create a NuGet package called after the name of the module and the version found in the nuspec file. To get the version, the script parses the nuspec file for the package.metadata.version data. 
Then the script creates a local source configuration with the URI of the Azure Artifact NuGet endpoint. You can find this URI by using “Connect to the feed” and selecting NuGet on the Azure DevOps Artifact page. 
The last command sends the package to the Azure artifact feed. 
As you can see, the SYSTEM_ACCESSTOKEN environment variable is used as the username and password for the NuGet source configuration. This variable is sent to the script by the pipeline configuration.

To run it inside an Azure pipeline, you will need a yaml pipeline definition.  

```yaml
trigger:
- master

pool:
  vmImage: ubuntu-latest

stages:
- stage: buildandpackage
  jobs:
  - job: buildandpackagejob
    steps:
    - task: PowerShell@2
      inputs:
        filepath: '$(Build.SourcesDirectory)/build/build.ps1'
        pwsh: true
        failOnStderr: true
      env: 
        SYSTEM_ACCESSTOKEN: $(System.AccessToken)
```

The first statement, trigger, enables the pipeline each time you push or make a pull request on the master branch.

The second bloc indicates that we will use an Ubuntu machine

The last bloc will execute the script with the help of the System Access Token to access Azure Artifact. 

But how can we use Azure Factory as a private PowerShell registry?

You will need to create a Personal Access Token for the account that will install the module and you will need to use NuGet. 
But as there is a bug in PowerShell (currently), you can’t use the v3 API and you will need to use the v2. 


As we don’t want to store the PAT in the script, the PAT will be passed as an argument. 
There is a need to force PowerShell to use TLSv3

To create the credential needed to login to the API, the PAT needs to be converted to a secure string. This secure string, along with the PAT will be used to create the credential 

```powershell
[CmdletBinding()]
param (
    [Parameter(Mandatory=$true)]
    [string]
    $AzTocken 
)

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls3

$SecureToken = $AzTocken | convertTo-SecureString -AsPlainText 

$AzArtifactCredential = New-Object System.Management.Automation.PSCredential($AzTocken, $SecureToken)

$AzArtifactFeedURI = "https://pkgs.dev.azure.com/<Organisation>/<Project>/_packaging/<FeedNqme>/nuget/v2"

Register-PSRepository -name PSPrivateModule -SourceLocation $AzArtifactFeedURI  -PublishLocation $AzArtifactFeedURI -InstallationPolicy Trusted -Credential $AzArtifactCredential 

install-module -Name <ModuleName> -Repository PSPrivateModule -Credential $AzArtifactCredential -Force
```

This is the startup point on how you can use Azure DevOps Artefact for your PowerShell Module.