Let us review why and how we use conditional expressions in our v2 application-type pipeline blueprints.
What is a conditional expression and why is it "cool"?
You can use the if
, elseif
, and else
clauses to conditionally assign values or, as discussed in this blog post, conditionally run a step when a condition is met.
Conditions are defined using Expressions and built-in Functions. We have made heavy use of conditional expressions to define what our blueprints assemble at queue (run) time, which is not only a powerful concept, but also allows us to tick off a couple of classic Azure Pipeline security risks.
# VARIABLES
variables:
- ${{ if ne(parameters.suppressCD, true) }}:
- template: /deploy/${{ lower(parameters.portfolioName) }}/${{ lower(parameters.productName) }}-config.yml@CeConfiguration
In the example above, we include a variable template if, and only if, the suppressCD
parameter is not set to true
. Note that we are intentionally turning every character in the variable template e to lowercase using the lower
function.
Examples in our blueprints
Conditional Quality Assurance and Security Scans
As described in our recent Azure Pipelines Blueprint QA Integration post, our pipelines are designed to target a lower and higher environment, whereby the higher is locked down and only included when the artifact originates from a release
branch.
Let us peel the example below, layer by layer, which is the template we are using for both the Security Automation and Quality Assurance (QA) stages.
- The conditional expression example
${{ if or(eq(variables['Build.SourceBranch'], 'refs/heads/release'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
decides if part of the template is included, by checking if the source branch is namedrelease
orrelease/*
, where*
is a semantic version using the MAJOR.MINOR format. For example:release/1.3
. - The template is divided into three sections of
steps
, the steps to run for LOWER environment artifacts, the steps for the HIGHER environment artifacts, and the steps to run for both the LOWER+HIGHER environments. All implemented usingconditional expressions
. - In the lower, higher, and both lower+higher placeholders, the conditional expression example
${{ if eq( lower(parameters.applicationBlueprint), 'azure-function' ) }}:
allows us to define application-specific steps to be run.
parameters:
- name: applicationBlueprint
type: string
- name: modeElite
type: boolean
default: false
steps:
# ------------------------------------------------------
# QA AUTOMATION FOR LOWER (NON-PROD) ENVIRONMENTS STAGE
# ------------------------------------------------------
- ${{ if not(or(eq(variables['Build.SourceBranch'], 'refs/heads/release'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'))) }}:
- script: echo 'QA CD Lower Environment Automation Placeholder ***'
- script: echo applicationBlueprint = ${{parameters.applicationBlueprint}}
- script: echo modeElite = ${{parameters.modeElite}}
- ${{ if eq( lower(parameters.applicationBlueprint), 'azure-function' ) }}:
- script: echo deal with qa scan relevant to azure-function application type
- ${{ if eq( lower(parameters.applicationBlueprint), 'nuget-package' ) }}:
- script: echo deal with qa scan relevant to nuget-package application type
- ${{ if eq( lower(parameters.applicationBlueprint), 'universal-artifact' ) }}:
- script: echo deal with qa scan relevant to universal-artifact application type
- ${{ if and(ne(lower(parameters.applicationBlueprint),'universal-artifact'), ne(lower(parameters.applicationBlueprint),'nuget-package'), ne(lower(parameters.applicationBlueprint),'azure-function')) }}:
- script: echo UNKNOWN application type
# ------------------------------------------------------
# QA AUTOMATION FOR HIGHER (PROD) ENVIRONMENTS STAGE
# ------------------------------------------------------
- ${{ if or(eq(variables['Build.SourceBranch'], 'refs/heads/release'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
- script: echo 'QA CD Higher Environment Automation Placeholder ***'
- script: echo applicationBlueprint = ${{parameters.applicationBlueprint}}
- script: echo modeElite = ${{parameters.modeElite}}
... rinse and repeat ...
# ------------------------------------------------------
# QA AUTOMATION FOR LOWER AND HIGHER (PROD) ENVIRONMENTS STAGE
# ------------------------------------------------------
- script: echo 'QA CD Lower and Higher Environment Automation Placeholder ***'
Simple, but powerful!
Boot-Strap Flow
Our boot-strap template is our secret sauce that injects DevSecOps, Building Code, Toolkits, and Application Insights steps into our continuous integration (CI) pipeline.
In the 80's I would have written the boot-strap logic as a gigantic Assembler or V2/PLM switch statement. Back to the future, we have conditional YAML expression that make the experience a lot more readable and user friendly q;-)
Here is a short extract from our bootstrap
template where we checkout our toolbox and call our Application Insights logging steps if the bootstrapMode
is set to init
.
... snipped code ...
======================================================
# BOOTSTRAP TOOLBOX
======================================================
# Production Toolbox
- ${{ if or( eq( lower(parameters.bootstrapMode), 'init' ), eq( lower(parameters.bootstrapMode), 'runbuildingcodeonly' )) }}:
- checkout: git://Common-Engineering-System/AzureDevOps.Automation.Pipeline.Toolbox.v2
======================================================
# BOOTSTRAP AI LOGGING
======================================================
- ${{ if eq( lower(parameters.bootstrapMode), 'init' ) }}:
- task: PowerShell@2
displayName: 'Bootstrap Telemetry START'
inputs:
filePath: '$(Build.SourcesDirectory)/AzureDevOps.Automation.Pipeline.Toolbox.v2/toolbox/scripts/application-insights/log-bootstrap-event-to-ai.ps1'
arguments: -OperationId "$(Build.BuildNumber).$(Build.BuildId)" `
-Event 'Bootstrap' `
-Action 'Start' `
-BootstrapMode ${{parameters.bootstrapMode}} `
-ApplicationType ${{parameters.applicationType}} `
-ApplicationBlueprint ${{parameters.applicationBlueprint}} `
-PortfolioName ${{parameters.portfolioName}} `
-ProductName ${{parameters.productName}} `
-SourcesDirectory ${{parameters.sourcesDirectory}} `
-VerboseFlag ${{parameters.verbose}} `
-ForceCheck ${{parameters.forceCheck}}
failOnStderr: true
pwsh: true
continueOnError: true
... snipped code ...
Conditional Templates
The last example is an extract from the azure-pipeline-nuget-package-ci.yml where we conditionally load a portfolioName/productName
specific or a default
variable template based on the value of the useDefaultConfig
parameter.
# VARIABLES
variables:
- ${{ if and( ne(parameters.suppressCD, true), ne(parameters.useDefaultConfig, true) ) }}:
- template: /deploy/${{ lower(parameters.portfolioName) }}/${{ lower(parameters.productName) }}-config.yml@CeConfiguration
- ${{ if and( ne(parameters.suppressCD, true), eq(parameters.useDefaultConfig, true) ) }}:
- template: /deploy/default.config/nuget-package-config.yml@CeConfiguration
Lastly, a spacing GOTCHA!
Finally, let us look at a common gotcha. YAML is very space sensitive and the indents are important. For example, only the first two script
steps are part of the conditional expression context. The third script
, which is not indented will be run irrespective of the conditional expression result.
We strongly recommend you use Visual Studio Code to give visual cues of your code and grouping thereof and the Azure Pipeline YAML Validate feature to validate that your syntax is correct.
- ${{ if not(or(eq(variables['Build.SourceBranch'], 'refs/heads/release'), startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'))) }}:
- script: echo 'Security CD Lower Environment Automation Placeholder ***'
- script: echo applicationBlueprint = ${{parameters.applicationBlueprint}}
- script: echo modeElite = ${{parameters.modeElite}}
That is, it for today folks! Ping me on @wpschaub if you have any questions or feedback.