Using PowerShell in Azure

Let’s start by saying the PowerShell is one of the most opaque and crappy languages that was ever created. I’m glad some people like it – but as far as me, it sucks.

  • Why have two different syntaxes for environment variables – sometimes “$env” and sometimes “env”.
  • Why special string escape character (backtick – “`”). Tradition escape character (“\”) is used in so many languages: Bash, C, C++, Java, PHP, Python, Ruby, SQL, Verilog.

Adding PowerShell to Azure YAML Build Script

Adding PowerShell scripts is easy. The example below shows how to create a script with embedded PowerShell code.

- task: PowerShell@2
  displayName: 'Copy necessary generated files to artifacts.'
  inputs:
    targetType: 'inline'
    script: |
      Write-Host "Hello, world"

You can easily add PowerShell templates that can be called from other scripts by providing any necessary input parameters.

parameters:
  # "Major.Minor.Build"
- name: mmb      
  type: string
  
steps:
    # Compute build number parts and if is Fortify build.
    - task: PowerShell@2
      displayName: 'Process build information'  
      inputs: 
        targetType: 'inline'
        script: |
          write-host "mmb: ${{parameters.mmb}}"  # Echo value into build log.

Example YAML Script Using PowerShell

Azure has numerous rules for accessing variable in a PowerShell script. Below are examples in a YAML file.

variables:
  BuildPlatform: 'Any CPU'
  DropStagingDirectory: '$(Build.ArtifactStagingDirectory)/drop'

...
jobs:
  - job: BuildAll
    workspace:
      clean: all
    pool:
      name: '...'

  - steps
    - checkout: self
    - checkout: 'other repo'

    # Create artifact drop folder.
    - task: PowerShell@2
      displayName: 'Drop: Create dest folder'
      inputs:
        targetType: inline
        script: |
          Write-Host "Build.ArtifactStagingDirectory: $(Build.ArtifactStagingDirectory)"  # Echo value to build log
          $path = "$(Build.ArtifactStagingDirectory)\drop"
          if( -Not (Test-Path -Path "$(path)\drop"))
          {
            Write-Host "Creating drop folder"
            New-Item -path "$(Build.ArtifactStagingDirectory)" -name "drop" -ItemType "directory"
            Write-Host "Created folder: $(Build.ArtifactStagingDirectory)/drop/"
          }
          else
          {
            Write-Host "Folder already exists: $(Build.ArtifactStagingDirectory)/drop/"
          }

Access Variables and Environment Settings

From Microsoft: In a pipeline, template expression variables (${{ variables.var }}) get processed at compile time, before runtime starts. Macro syntax variables ($(var)) get processed during runtime before a task runs. Runtime expressions ($[variables.var]) also get processed during runtime but are intended to be used with conditions and expressions. When you use a runtime expression, it must take up the entire right side of a definition.

# Copy current value of Build.SourceBranch to local variable.
$sourceBranch = "$(Build.SourceBranch)"

# Access environment variable.
$branchname = "$env:BUILD_SOURCEBRANCH"
$platform = "$env:BuildPlatform"

# Copy initial value of variable locally.
#   This will be value before ANY changes by "##vso[task.setvariable variable=x]"
$sourceBranch = "${{Build.SourceBranch}}"

PowerShell Strings and Variables

As in the previous section, you can embed variables in strings. Be aware that if the variable name has trailing characters, PowerShell can’t figure out the variable you want to embed. Also, if you misspell a variable name, PowerShell will simply output an empty string in that location.

$sourceBranch = "$(Build.SourceBranch)"

# 
Write-Host "sb: $sourceBranch--$(Build.SourceBranch)"
# Outputs: "sb: refs/heads/private/bob/it_FDC--refs/heads/private/bob/it_FDC"

HWrite-Host "sb: $sourceBranchff--$(Build.SourceBranch)"
# Output: "sb: --refs/heads/private/bob/it_FDC"
#    Notice first variable ($sourceBranchff) doesn't exist so rendered as "".

Write-Host "sb: $($sourceBranch)ff--$(Build.SourceBranch)"
# Output: "sb: refs/heads/private/bob/it_FDCff--refs/heads/private/bob/it_FDC"
#    Wrapped $sourceBranch in $() so trailing "ff" is ignored when determining the variable name.

The same rules apply to environment variables – but you must use the correct “$env” formatted name. PowerShell is not case sensitive, but you must change the “.” characters in the names to “_” characters. For example, the variable Build.SourceBranch becomes the variable Build_SourceBranch.

# Use this line to display all environment variables to console. 
Get-ChildItem env: 
# Example output. Includes 310 items for me.
#    BUILD_ARTIFACTSTAGINGDIRECTORY C:\a\1\a                                                                                
#    BUILD_BINARIESDIRECTORY        C:\a\1\b                                                                                
#    BUILD_BUILDID                  476273                      

Write-Host "sb: $($env:BUILD_SOURCEBRANCH)ff--$(Build.SourceBranch)"
# Output: "sb: refs/heads/private/bob/it_FDCff--refs/heads/private/bob/it_FDC"

Write-Host "sb: $env:BUILD_SOURCEbranch--$(Build.SourceBranch)"
# Output: "sb: refs/heads/private/bob/it_FDC--refs/heads/private/bob/it_FDC"

Modify Variables

You can change the value of variables and environment settings by using the task.setvariable. The simplest approach is to modify the value and in later tasks, the new value will be used if the code uses macro syntax ($(var)). Note that the original values is still available as ${{ var }}.

# Update CG_VERSION_REV with value of "$revision".
Write-Host "##vso[task.setvariable variable=CG_VERSION_REV;]$revision

Determine If Environment Variable Exists and Is Not Empty String

In my case, I want to determine if a variable exists in the environment. I don’t care what its value is since I’m using its existence as a flag. Important to note is that environment variables are always string types. If the current value of variable is “”, then “Test-Path” will return $False.

# If "FORCE_FORTIFY_BUILD" exists in environment, set variable to $True.
#  (We don't care about value - just that it exists.)
$forceFortifyBuild = Test-Path env:FORCE_FORTIFY_BUILD

# If user wants debug information added to build (by checking "Enable system diagnostics"), 
#    variable will be set to "True".
$debugBuild = Test-Path env:SYSTEM_DEBUG