Azure DevOps and "The term 'Install-Module' is not recognized" issue
Tags: devops, azure
Published: 2024-07-17 06:47:11

Just a quick note in case it will help someone. =)

## The problem
Since somewhere beginning of July, I started noticing that my pipeline if failing **randomly** with `The term 'Install-Module' is not recognized as a name of a cmdlet, function, script file, or executable program.` error.

The pipeline is configured as follows:

### mypipeline.yaml
```yaml 
- job: ExportLists
    displayName: Export Lists from ${{ parameters.exportFrom }}
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: AzurePowerShell@5
      name: DeploySPFx
      inputs:
        azureSubscription: $(serviceConnection)
        azurePowerShellVersion: LatestVersion
        ScriptType: FilePath
        ScriptPath: $(Build.SourcesDirectory)/Pipelines/scripts/SPO-ExportLists.ps1
        ScriptArguments: >
          -tenantName '$(Az_TenantName)'
          -siteName '$(SPO_SiteName)'
          -folderPath '$(Build.SourcesDirectory)/SPO/templates'
      displayName: Export lists
```

The PowerShell script WAS like that:

###ExportLists.ps1
```powershell
[CmdletBinding()]
param(
    $tenantName,
    $siteName,
    $folderPath
)

Install-Module -Name PnP.PowerShell -Scope CurrentUser `
   -SkipPublisherCheck -Force

#.....
```

And depending on the day the script sometimes worked, sometimes it didn't.
Occasionally, different jobs in the same stage would either succeed or fail on the `Install-Module` command.

## Troubleshooting

According to the [Ubuntu 22.04](https://github.com/actions/runner-images/blob/ubuntu22/20240624.1/images/ubuntu/Ubuntu2204-Readme.md), this image comes with **PowerShell 7.4.3**, and 

>PowerShell 7.4 includes **Microsoft.PowerShell.PSResourceGet** v1.0.1. This module is installed side-by-side with **PowerShellGet **v2.2.5 and **PackageManagement **v1.4.8.1. 

I see no reason for `Install-Module` failing, and ... why so randomly.

## The solution

I changed my code to call `Get-Module` and now the script looks like that:

###ExportLists.ps1
```powershell
[CmdletBinding()]
param(
    [string]$tenantName,
    [string]$siteName,
    [string]$folderPath
)

Write-Host "##[group]Install PS modules"
Write-Host "##[command] Get Module PowerShellGet"
Get-Module -Name PowerShellGet -ListAvailable
Write-Host "##[command] Get Module PowerShellGet"
Get-Module -Name Microsoft.PowerShell.PSResourceGet -ListAvailable
Write-Host "##[command] Install PnP.PowerShell"
Install-Module -Name PnP.PowerShell -Scope CurrentUser -SkipPublisherCheck -Force

Write-Host "##[endgroup]"
```
And.. it works. Since whole three days already! Let's hope it will stay this way.🤞

Can someone explain it to me?

## Bonus content
If you are using **PnP.PowerShell** in your pipeline, and you have a Service Connection with Azure, you **don't have to authenticate to connect to SharePoint**. 

The pipeline already runs in the context of the Service Principal.
**AzurePowerShell@5** accepts service connection as a parameter, and also runs in the context of the service principal.

All you need to do it to refresh the token in your PS script. Let me show you.

To get authentication context from the pipeline, you would call `Get-AzAccessToken` and connect to your SharePoint site using `Connect-PnPOnline -Url $url -AccessToken $azAccessToken.Token`.

###SPO-Auth.ps1
```powershell
function Connect-SPOSite {
param(
    [Parameter(Mandatory = $true)]
    [string] $tenantName,
    [Parameter(Mandatory = $true)]
    [string] $siteName
)

Write-Host "##[group]Who am I"
$azContext = (Get-AzContext).Account.Id
$sp = Get-AzADServicePrincipal -ApplicationId $azContext
Write-Host "##[debug] ServicePrincipal: $($sp.Id)"
Write-Host "##[endgroup]"

$url = "https://$tenantName.sharepoint.com"
Write-Host "##[debug]Connecting to $url"

try {
    $azAccessToken = Get-AzAccessToken -ResourceUrl $url
    Write-Host "##[debug]$azAccessToken"

    $conn = Connect-PnPOnline -Url "$url/sites/$siteName" -AccessToken $azAccessToken.Token -ReturnConnection
    Write-Host "##[debug]Get-PnPConnection"
    Write-Host "##[debug]$($conn.Url)"

    return $conn
}
catch {
    Write-Host "##[error]$($_.Exception.Message)"
}
}
```

From this moment on, your PnP commands may use the connection returned by SPO-Auth.ps1:

###ExportLists.ps1
```powershell

[CmdletBinding()]
param(
    $tenantName,
    $siteName,
    $folderPath
)

Write-Host "##[group]Install PS modules"
Write-Host "##[command] Get Module PowerShellGet"
Get-Module -Name PowerShellGet -ListAvailable
Write-Host "##[command] Get Module PowerShellGet"
Get-Module -Name Microsoft.PowerShell.PSResourceGet -ListAvailable
Write-Host "##[command] Install PnP.PowerShell"
Install-Module -Name PnP.PowerShell -Scope CurrentUser -SkipPublisherCheck -Force

Import-Module "$PSScriptRoot/_SPO-Auth.ps1"
Write-Host "##[endgroup]"

# This is the call to SPO-Auth.ps1
Write-Host "##[debug]Connecting to SharePoint Online"
$conn= Connect-SPOSite -tenantName $tenantName -siteName $siteName

# And here your code would start:
Get-PnPSiteTemplate -Out "$folderPath/SiteFields.xml" -Handlers Fields -connection $conn

```

If you are not using Workload Identity federation yet, have a look at my previous post [Deploy SPFx app using pipeline's Workload Identity federation](https://dev.to/kkazala/deploy-spfx-app-using-pipelines-workload-identity-federation-5fhi)