powershellazure-devopsazure-pipelines

How to run Powershell -Parallel feature on Azure DevOps Pipelines?


I am trying to run a powershell script that uses the -Parallel feature in my Powershell script that is being called at DevOps pipeline, but I am getting an error message that I am not finding how to solve.

This is my pipeline:

trigger:
  branches:
    include:
      - develop

variables:
  ROOT: $(Build.SourcesDirectory)
  REPOROOT: $(Build.SourcesDirectory)
  OUTPUTROOT: $(REPOROOT)

  WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' # Docker image which is used to build the project https://aka.ms/obpipelines/containers
  LinuxContainerImage: 'mcr.microsoft.com/onebranch/cbl-mariner/build:2.0'

resources:
  repositories: 
    - repository: templates
      type: git
      name: OneBranch.Pipelines/GovernedTemplates
      ref: refs/heads/main

extends:
  template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates # https://aka.ms/obpipelines/templates
  parameters:
    stages:
    - stage: Build
      jobs:
      - job: Generate_Synapse_Arm_Templates
        pool:
          # This job must run in a Linux container because the Synapse workspace deployment task fails when run in a Windows container
          type: linux
        variables:
          ob_outputDirectory: '$(REPOROOT)'
          ob_artifactBaseName: "drop_synapse"

        steps:
          - task: Synapse workspace deployment@2
            displayName: 'Generate Synapse Template Files'
            inputs:
              operation: 'validate'
              ArtifactsFolder: '$(Build.SourcesDirectory)/AzureSynapseSpark/wcxsynapseusdev'
              continueOnError: false
              TargetWorkspaceName: wcxsynapseusdev

          - task: CopyFiles@2
            displayName: 'Copy Synapse Template Files'
            inputs:
              SourceFolder: $(Build.SourcesDirectory)/ExportedArtifacts/
              Contents: '**'
              TargetFolder: $(ob_outputDirectory)
      
          # Update the Ev2 artifact version with the build number
          - task: PowerShell@2
            displayName: 'Set Ev2 service artifacts version'
            inputs:
              targetType: 'inline'
              script: '$(Build.BuildNumber) | Out-File "$(Build.SourcesDirectory)\src\ev2_service_artifacts\version.txt" -Encoding ascii'

    - stage: Bicep_Build
      displayName: 'Build Bicep Files'
      dependsOn: []
      jobs:
      - job: Build_Bicep
        pool:
          name: "AVD_1ES_POOL_PROD"
          type: windows
          demands: sqlpackage
          hostVersion: 1ESWindows2022
        variables:
          ob_outputDirectory: '$(Build.SourcesDirectory)/out'
          ob_pipelineartifacts_enabled: false
          WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest'
          ob_artifactBaseName: "ARM_Templates"
        steps:
          - checkout: self

          - task: PowerShell@2
            displayName: 'List files'
            inputs:
              targetType: 'inline'
              script: 'Get-ChildItem -Path $(Build.SourcesDirectory)/boundary-data-node'

          - task: PowerShell@2
            displayName: 'Clear .azure folder to prevent access issues'
            inputs:
              targetType: 'inline'
              script: |
                $azureFolder = "$env:USERPROFILE\.azure"
                if (Test-Path $azureFolder) {
                    Get-ChildItem $azureFolder -Recurse -Force | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue
                    Write-Host "Cleared contents of $azureFolder instead of deleting it."
                }
                else {
                    Write-Host "$azureFolder does not exist."
                }

          - task: PowerShell@2
            displayName: 'Format Bicep files'
            inputs:
              pwsh: true
              targetType: 'filepath'
              filePath: '$(Build.SourcesDirectory)/boundary-data-node/.scripts/BuildBicep.ps1'
              arguments: 'format -TemplatePath "$(Build.SourcesDirectory)/boundary-data-node/src/ev2_service_artifacts/templates" -ParameterPath "$(Build.SourcesDirectory)/boundary-data-node/src/parameters"'

          - task: PowerShell@2
            displayName: 'Lint Bicep files'
            inputs:
              pwsh: true
              filePath: '$(Build.SourcesDirectory)/boundary-data-node/.scripts/BuildBicep.ps1'
              arguments: 'lint -TemplatePath "$(Build.SourcesDirectory)/boundary-data-node/src/ev2_service_artifacts/templates" -ParameterPath "$(Build.SourcesDirectory)/boundary-data-node/src/parameters"'

          - task: PowerShell@2
            displayName: 'Build Bicep files'
            inputs:
              pwsh: true
              filePath: '$(Build.SourcesDirectory)/boundary-data-node/.scripts/BuildBicep.ps1'
              arguments: 'build -TemplatePath "$(Build.SourcesDirectory)/boundary-data-node/src/ev2_service_artifacts/templates" -ParameterPath "$(Build.SourcesDirectory)/boundary-data-node/src/parameters"'

This is my error:

Starting: Format Bicep files (windows_build_container)
==============================================================================
Task         : PowerShell
Description  : Run a PowerShell script on Linux, macOS, or Windows
Version      : 2.247.1
Author       : Microsoft Corporation
Help         : https://docs.microsoft.com/azure/devops/pipelines/tasks/utility/powershell
==============================================================================
Generating script.
Formatted command: . 'C:\__w\1\s\boundary-data-node\.scripts\BuildBicep.ps1' format -TemplatePath "C:\__w\1\s/boundary-data-node/src/ev2_service_artifacts/templates" -ParameterPath "C:\__w\1\s/boundary-data-node/src/parameters"
========================== Starting Command Output ===========================
"C:\powershell-7\pwsh.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command ". 'C:\__w\_temp\4b0a0cb6-0a48-449f-ae08-22d7a1d338af.ps1'"
No .git directory found in the directory hierarchy.
Task started at: 03/31/2025 18:43:53

Running Format task...
--------------------------
Processing templates files from folder 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates':
  - Formatting file: 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates\adf.deploymentTemplate.bicep'
  - Formatting file: 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates\applicationInsights.deploymentTemplate.bicep'
  - Formatting file: 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates\logAnalyticsWorkspace.deploymentTemplate.bicep'
  - Formatting file: 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates\machineLearningWorkspace.deploymentTemplate.bicep'
  - Formatting file: 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates\machineLearningWorkspaceDataStores.deploymentTemplate.bicep'
  - Formatting file: 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates\roleAssignments.deploymentTemplate.bicep'
  - Formatting file: 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates\storage.deploymentTemplate.bicep'
  - Formatting file: 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates\roleAssignmentsRoleId.deploymentTemplate.bicep'
  - Formatting file: 'C:\__w\1\s\boundary-data-node\src\ev2_service_artifacts\templates\userManagedIdentity.deploymentTemplate.bicep'
Traceback (most recent call last):   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/_session.py", line 37, in load FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\ContainerAdministrator\\.azure\\azureProfile.json' System.Management.Automation.RemoteException During handling of the above exception, another exception occurred: System.Management.Automation.RemoteException Traceback (most recent call last):   File "<frozen runpy>", line 198, in _run_module_as_main   File "<frozen runpy>", line 88, in _run_code   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/__main__.py", line 30, in <module>   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/__init__.py", line 929, in get_default_cli   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/__init__.py", line 80, in __init__   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/_session.py", line 50, in load   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/_session.py", line 54, in save PermissionError: [Errno 13] Permission denied: 'C:\\Users\\ContainerAdministrator\\.azure\\azureProfile.json'
Traceback (most recent call last):   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/_session.py", line 37, in load FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\ContainerAdministrator\\.azure\\az.json' System.Management.Automation.RemoteException During handling of the above exception, another exception occurred: System.Management.Automation.RemoteException Traceback (most recent call last):   File "<frozen runpy>", line 198, in _run_module_as_main   File "<frozen runpy>", line 88, in _run_code   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/__main__.py", line 30, in <module>   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/__init__.py", line 929, in get_default_cli   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/__init__.py", line 81, in __init__   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/_session.py", line 50, in load   File "D:\a\_work\1\s\build_scripts\windows\artifacts\cli\Lib\site-packages\azure/cli/core/_session.py", line 54, in save PermissionError: [Errno 13] Permission denied: 'C:\\Users\\ContainerAdministrator\\.azure\\az.json'

This is my Powershell script:

param(
    [string]$Mode,
    [string]$TemplatePath = $null,
    [string]$ParameterPath = $null
)

# Global filter for all Bicep files
$filter = "*.bicep*"

function CheckPowerShellVersion() {
    $minVersion = 7
    $currentVersion = $PSVersionTable.PSVersion.Major

    if ($currentVersion -lt $minVersion) {
        throw "Error: This script uses the -Parallel feature requiring '$minVersion' or higher. 
        Documentation feature: https://devblogs.microsoft.com/powershell/powershell-foreach-object-parallel-feature/)."
    }
}

# Function to retrieve Bicep files based on a given path
function GetBicepFiles($path) {
    return Get-ChildItem -Path $path -Filter $filter -Recurse
}

# Function to build Bicep files
function BuildBicepFiles($path, $isParam = $false) {
    $files = GetBicepFiles -path $path
    $files | ForEach-Object -Parallel {
        $buildCommand = if ($using:isParam) { @("az", "bicep", "build-params") } else { @("az", "bicep", "build") }
        $outputDir = $_.DirectoryName
        if (-not (Test-Path -Path $outputDir)) {
            New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
        }
        Write-Host "  - Building file: '$($_.FullName)' into '$outputDir'"
        & $buildCommand[0] $buildCommand[1] $buildCommand[2] --file $_.FullName --outdir $outputDir

        if ($LASTEXITCODE -ne 0) {
            throw "Error: Failed to build file '$($_.FullName)'"
        }        
    } -ThrottleLimit 50
}

# Function to format Bicep files
function FormatBicepFiles($path) {
    $files = GetBicepFiles -path $path

    $files | ForEach-Object -Parallel {
        Write-Host "  - Formatting file: '$($_.FullName)'"
        $output = az bicep format --file $_.FullName 2>&1
        Write-Host $output

        if ($LASTEXITCODE -ne 0) {
            throw "Error: Failed to format file '$($_.FullName)'"
        }
    } -ThrottleLimit 50
}

# Function to lint Bicep files
function LintBicepFiles($path) {
    $files = GetBicepFiles -path $path

    $files | ForEach-Object -Parallel {
        Write-Host "  - Linting file: '$($_.FullName)'"
        $output = & az bicep lint --file $_.FullName 2>&1
        Write-Host $output

        if ($LASTEXITCODE -ne 0) {
            throw "Error: Failed to lint file '$($_.FullName)'"
        }
    } -ThrottleLimit 50
}

# Unified function to process both templates and parameters path
function ProcessAllBicepFiles($processFunction) {
    function ProcessFolder($path, $isParam = $false) {
        if (Test-Path -Path $path) {
            # Run the process function for the current folder
            & $processFunction $path $isParam

            # Recurse into subfolders
            Get-ChildItem -Path $path -Directory | ForEach-Object {
                ProcessFolder -path $_.FullName -isParam $isParam
            }
        }
    }

    # Process templates path
    Write-Host "--------------------------"
    Write-Host "Processing templates files from folder '$baseTemplatePath':"
    ProcessFolder -path $baseTemplatePath -isParam $false    

    # Process parameters path
    Write-Host "                           "
    Write-Host "Processing parameters files from folder '$baseParameterPath'"
    ProcessFolder -path $baseParameterPath -isParam $true
}

# Function to run the selected task based on mode
function RunTask($mode) {
    # Ensure PowerShell version is compatible before running tasks
    CheckPowerShellVersion

    $overallStart = Get-Date
    Write-Host "Task started at: $overallStart`n"

    switch ($mode.ToLower()) {
        "build" {
            Write-Host "Running Build task..."
            ProcessAllBicepFiles -processFunction ${function:BuildBicepFiles}
        }
        "format" {
            Write-Host "Running Format task..."
            ProcessAllBicepFiles -processFunction ${function:FormatBicepFiles}
        }
        "lint" {
            Write-Host "Running Lint task..."
            ProcessAllBicepFiles -processFunction ${function:LintBicepFiles}
        }
        "full" {
            Write-Host "Running Full process: Format, Lint and Build..."
            ProcessAllBicepFiles -processFunction ${function:FormatBicepFiles}
            ProcessAllBicepFiles -processFunction ${function:LintBicepFiles}
            ProcessAllBicepFiles -processFunction ${function:BuildBicepFiles}
        }
        default {
            Write-Host "Invalid mode specified. Use 'Build', 'Format', 'Lint', or 'Full'."
        }
    }

    $overallEnd = Get-Date
    $totalElapsed = $overallEnd - $overallStart
    Write-Host "`nTotal execution time: $totalElapsed"
}

function Get-GitRoot {
    param (
        [string]$startDir = (Get-Location)
    )

    $currentDir = Get-Item -Path $startDir

    while ($currentDir -and -not (Test-Path -Path (Join-Path -Path $currentDir.FullName -ChildPath ".git"))) {
        $currentDir = $currentDir.Parent
    }

    if ($currentDir) {
        return $currentDir.FullName
    } else {
        Write-Host "No .git directory found in the directory hierarchy."
        # This ensures the script does not crash if .git is missing.
        return $env:SystemDrive
    }
}

$gitRoot = Get-GitRoot
# Normalize paths to avoid issues with relative paths
$TemplatePath = [System.IO.Path]::GetFullPath($TemplatePath)
$ParameterPath = [System.IO.Path]::GetFullPath($ParameterPath)

# Set default paths if not provided
$baseTemplatePath = if ($TemplatePath) { $TemplatePath } else { Join-Path -Path $gitRoot -ChildPath "src\ev2_service_artifacts\templates" }
$baseParameterPath = if ($ParameterPath) { $ParameterPath } else { Join-Path -Path $gitRoot -ChildPath "src\ev2_service_artifacts\parameters" }

# Run the task based on user input
if (-not [string]::IsNullOrEmpty($Mode)) {
    RunTask -mode $Mode
} else {
    RunTask -mode "build"
}

Solution

  • The problem was in installing Az and az bicep seems like, updated tasks:

              - task: PowerShell@2
                displayName: 'Install Az Module'
                inputs:
                  pwsh: true
                  targetType: 'inline'
                  script: |
                    Install-Module -Name Az -Force -AllowClobber
                    az bicep install
                    az bicep version