When managing pipelines for large and complex repositories with multiple ‘Platforms’, each containing multiple apps and services, then the folder structure and variable strategy can be complicated. However, if this is done right, then the payoff for template reuse is dramatic.
Here, I outline my approach on the pipeline folder and YAML structure only. The variable structure allows for a full set of naming conventions to easily default across all your projects apps and delegate standards to organisation and platform levels. This, ideally, leaves only app specific configurations for your dev teams to manage and worry about.
This strategy rests on top of my general approach to structuring a mono-repository. For more details on that see Mono-repository folder structures.
Table of Contents
- Environments
- Repo Folder Structure
- Variable YAML files and pipeline strategy
- Example Variable Templates
- Full Function Pipeline Example
- Postman API Test Publish Pipeline
Environments
Any pipeline and variable strategy should work across all environments. The goal here is to have a single pipeline deploy everywhere with almost zero changes. Let’s outline what we mean by environments.
For an enterprise project, I would normally have a minimum of 6 environments. Sometimes, usually when there is complex integration testing and dependent environments, then it can be many more. My standard set are:
- Development (dev)
- Quality Assurance (qa)
- Systems Integration Test (sit)
- User Acceptance Test (uat)
- Pre-production (pre)
- Production (prod)
Why and what are these environments for?
Each environment has a specific purpose and a suit of tests that should be conducted at that environment.
As we progress through the environments with our deployments, the environments become more secure. This can easily break code, so you want to find that out before UAT. You also want to validate that what you have built is secure and again, find out before the customer starts playing with it. This can be confirmed by specific security tests.
The environments data and infrastructure should also start to get closer to of production, both power and scale. It’s all very well when an API responds sub-second in dev with a thousand records, but will it still when in production with twenty million? That is what gets tested in Pre-production with a series of Performance, Load and Soak tests…and ideally some chaos testing too.
For this illustration and avoid too much repetition we are going to be only use just 3. DEV, QA and PROD.
Repo Folder Structure
For the full details on how I structure a repository see Mono-repository folder structures. Here we will focus on just the Pipelines and Pipeline Variables.
Personally, I place all my pipeline files in one place, but there may be a rationale for have the pipeline YAML closer to the code it will build, test, and deploy too.
The folder structure below illustrates what this structure may look like for a mono-repository with two ‘platforms’, each of which may have several micro-services and UI’s (e.g., multiple elements with independent development and deployment cadences and versions).
Variable YAML files and pipeline strategy
If the following strategy is followed correctly then only very minor changes will be required between environments, and pipelines will be much easier to maintain and reuse.
Azure DevOps has a Variable Library facility.
Although very convenient, there is currently no way to track changes to Libraries, so I limit their use to Key Vault reference Libraries. For all other pipeline variables, we use the variable YAML files.
For guidance on variable reuse in Azure DevOps see
This strategy does not apply to GitHub Actions. For guidance on variable reuse in GitHub Actions see
- Variables – GitHub Docs
- Creating configuration variables for an organization
- Creating configuration variables for a repository
- Creating configuration variables for an environment
Variable Template Files Structures
The root of the Variables folder can be found at /pipelines/variables
The structures and preference order of where to place variables is as follows:
By nesting variable templates, and reusing variables throughout the hierarchy and combining this with standardized naming convention (For Azure Resource Naming convention see Cloud Resource Naming Convention (Azure)), you can configure a whole host of variables at the root common-vars.yml
. This means that you will only need exceptions the further down the levels you go.
Example Variables
The following variables are typical examples. Here to illustrate the hierarchy we’ll follow the examples given in the Mono-repository folder structures.
So,
- Organisation is ‘My Organisation’. Short-code: ‘myorg‘
- The platform we will illustrate is ‘my-platform-1’. Short-code ‘mp1‘
- The app we will illustrate is ‘microservice1’ on this platform. Short-code ‘micro1‘
- We want everything to deploy to North Europe, short-code ‘ne‘
Top Level Common Variables
# These variables should work across all projects as defaults
variables:
Location: "northeurope"
LocationShortCode: "ne"
OrgShortCode: "myorg"
PlatformShortCode: "core"
StorageAccountName: $(OrgShortcode)$(PlatformShortCode)sa$(EnvShortCode)$(LocationShortCode)
FunctionAppName: $(OrgShortcode)-$(PlatformShortCode)-func-$(AppName)-$(EnvShortCode)-$(LocationShortCode)
FunctionAppResourceGroupName: $(OrgShortcode)-$(PlatformShortCode)-rg-$(EnvShortCode)-$(LocationShortCode)
SQLServerName: $(OrgShortcode)-$(PlatformShortCode)-sql-$(EnvShortCode)-$(LocationShortCode).database.windows.net
CosmosDbAccountName: $(OrgShortcode)-$(PlatformShortCode)-cosmos-$(EnvShortCode)-$(LocationShortCode)
AppInsightsName: $(OrgShortcode)-$(PlatformShortCode)-appi-$(EnvShortCode)-$(LocationShortCode)
KeyVaultName: $(OrgShortcode)-$(PlatformShortCode)-kv-$(EnvShortCode)-$(LocationShortCode)
KeyVaultResourceGroupName: $(OrgShortcode)-$(PlatformShortCode)-rg-kv-$(EnvShortCode)-$(LocationShortCode)
SonarCloudAccount: "My-SonarCloud-Acc@email.com"
SonarCloudOrganisation: "mysonarcloudorg"
WebAppName: $(OrgShortcode)-$(PlatformShortCode)-app-web-$(EnvShortCode)-$(LocationShortCode)
ApiAppName: $(OrgShortcode)-$(PlatformShortCode)-app-$(AppName)-$(EnvShortCode)-$(LocationShortCode)
PulumiBackendStorageAccountName: $(OrgShortcode)$(PlatformShortCode)sapulumi$(SubscriptionShortCode)$(LocationShortCode)
PulumiBackendStorageContainerName: $(PulumiProjectName)-state
PulumiBackendKeyVault: nerr-kv-pul-$(SubscriptionShortCode)-ne
PulumiStackName: "$(EnvShortCode)"
You may be surprised to see so many variables for resources lower down the tree. These are really the templates that have their variables populated and/or overridden later.
We can define the names of most resources, by using a convention (see Cloud Resource Naming Convention (Azure)) and then setting the $(AppName)
variable in the App Level variables file and the $(EnvShortCode)
set in the top-level environment variables as shown below. This works with every function app in any platform in any environment.
e.g. FunctionAppName: $(OrgShortcode)-$(PlatformShortCode)-func-$(AppName)-$(EnvShortCode)-$(LocationShortCode)
All but the AppName variable can be set at the organisation of platform level.
Top Level Environment Variables
# These variables should work across all projects as defaults for the DEV Environments
variables:
AzureSubscriptionConnection: "MYORG-Dev"
EnvironmentName: "Development"
EnvShortCode: "dev"
SubscriptionShortCode: "dev"
SubscriptionId: "a6f824a0-2a4b-4d53-9e87-8c34fee04158"
# These variables should work across all projects as defaults for the DEV Environments
variables:
AzureSubscriptionConnection: "MYORG-Prod"
EnvironmentName: "Production"
EnvShortCode: "prod"
SubscriptionShortCode: "prod"
SubscriptionId: "affd2308-294c-42ec-916b-d1328ac7ee20"
These top-level variables flow down to the App and can be used in app level pipelines. See the example templates below.
Platform Level Common Variables
# Platform Common Variables
variables:
PlatformName: "My Platform 1"
PlatformShortCode: "mp1"
PulumiProjectName: "mp1"
PulumiWorkingDirectory: "infrastructure/my-platform1-1/mp1stack"
Platform Level Environment Variables
# Platform Environment Variables for DEV
variables:
DevOpsEnvName: "MP1-DEV"
# Platform Environment Variables for PROD
variables:
DevOpsEnvName: "MP1-PROD"
Workload or App Common Variables
# App Common Variables
variables:
AppName: "micro1"
AppSrcRootPath: "src/my-platform-1/microservice1/myorg.platform1.microservice1.api/"
AppNamespace: "myorg.platform1.microservice1.api"
DbConnectionString: "Data Source=$(SQLServerName);Initial Catalog=$(DatabaseName);Authentication=Active Directory Managed Identity"
DatabaseName: "micro1db"
SonarCloudProjectKey: "MyAzDoOrg_myorg_my-platform1_microservice1"
SonarCloudProjectName: "myorg_my-platform1_microservice1"
When you look at the organisation level common variables, you will see they reference variables here, such as AppName.
Workload or App Environment Variables
# App DEV Variables
variables:
MyDependencyURL: https://mydepedency-test.someapi.com
# App PROD Variables
variables:
MyDependencyURL: https://mydepedency.someapi.com
If you push your variables to use nested variables and see if they can be closer to the root, you will find that there are very few variables to configure at the App environment level.
Using Variable Templates
To use the variable template files in a pipeline, we need to reference the correct ones in the correct place
Pipeline Level (top)
variables:
# This variable template is common across the whole Organisation for all environments
- template: /pipelines/variables/common.yaml
# This variable template is common across the Platform for all environments
- template: /pipelines/variables/my-platform-1/common.yaml
# This variable template is common across the App components for all environments
- template: /pipelines/variables/my-platform-1/microservice1/common.yaml
Stage Level (Environment)
Each environment is deployed in a stage. One after the other. See below for the full pipeline examples.
variables:
# This variable template is common across the whole Organisation for a single environment
- template: /pipelines/variables/common-vars-${{ env }}.yaml
# This variable template is common across the whole Platform for a single environment
- template: /pipelines/variables/my-platform-1/${{ env }}-vars.yaml
# This variable template is common across the whole App components for a single environment
- template: /pipelines/variables/my-platform-1/microservice1/${{ env }}-vars.yaml
Reference Variables in variable Templates
As the variable template files, are YAML pipeline templates, they in turn can reference other variable templates. In this way it is possible to have the variable templates encapsulate the hierarchy and then only reference the most specific one in the CICD pipeline itself.
# File: azure-pipelines.yml
variables:
- template: vars.yml # Template reference
steps:
- script: echo My favorite vegetable is ${{ variables.favoriteVeggie }}.
App Configuration Variables
Application variables for Functions and Web App deployments can be kept in a separate variable file. This is usually so specific to the app, that it is likely to be in the app folder and called app-config-vars.yml
.
Here the functions app configuration settings are listed as key-value pairs for the deployment step in a pipeline variable called ‘appSetting‘.
variables:
appSettings: |
-FUNCTIONS_WORKER_RUNTIME "dotnet-isolated"
-OrderDbOrdersContainer $(OrderDbOrdersContainerName)
-OrderDbOrderDetailsContainer $(OrderDbOrderDetailsContainerName)
-OrderDbDatabase $(OrderDbDatabaseName)
-OrderDbKey "@Microsoft.KeyVault(SecretUri=https://$(KeyVaultName).vault.azure.net/secrets/cosmosdb-primary-rw-key/)"
-OrderDbUri $(CosmosDbUri)
-AdTenantId $(AdTenantId)
-AdClientId $(AdClientId)
-AdClientSecret "@Microsoft.KeyVault(SecretUri=https://$(KeyVaultName).vault.azure.net/secrets/ad-client-secret-order/)"
and referenced directly into a variable as follows:
variables:
- name: FuncVariablesTemplate
value: variables/my-platform-1/microservice1/app-config-vars.yml
and used like:
steps:
- ${{ if eq(parameters.DeployToSlots, true) }}:
- task: AzureFunctionApp@1
displayName: API Funcion App Deployment - $(AppNamespace)
inputs:
azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
appType: 'functionApp'
appName: '$(FunctionAppName)'
deployToSlotOrASE: true
resourceGroupName: '$(FunctionAppResourceGroupName)'
slotName: 'staging'
package: '$(Pipeline.Workspace)/$(AppName)/$(AppName).zip'
appSettings: $(appSettings)
deploymentMethod: 'auto'
Example Variable Templates
Although this seems complicated, I think the best way to understand the reasons for this setup is by example. Below is a function combined CICD pipeline and the templates it references.
The magic now, is that this setup will work for any Function in any Platform in any organisation, with pretty much only a few variable changes.
Another advantage is that the pipelines have effectively been codified leaving your dev teams to only manage a few very local variables and setting close to the App code…which they are best placed to understand.
Full Function Pipeline Example
A Typical CICD Pipeline for an Azure Function
The following is a combined Continuous Integration (CI) and Continuous Delivery (CD) pipeline that deploys to Dev, then QA, then Prod.
It uses four pipeline templates, which are given below.
function-dotnet--ci-template.yml
for the build stagefunctions-cd-job-template.yml
in each Environment Stage to deploy the function- This template in turn incorporates a template for
- packaging postman API tests for the deployment called
publish-postman-ci-template.yml
- running postman API tests called
postman-runner-ci-template.yml
- packaging postman API tests for the deployment called
and references all the necessary pipeline variable YAML files.
# Pipeline for deploying code and configuration to an Azure Function - myorg.my-pltform-1.microservice1
parameters:
- name: buildConfiguration
displayName: Build Configuration
default: Release
type: string
values:
- Release
- Debug
- name: RunApiTests
displayName: Run the API integration tests with Postman
default: false
type: boolean
- name: RunApimTests
displayName: Run the APIM tests with Postman
default: false
type: boolean
name: $(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
variables:
- template: /pipelines/variables/common-vars.yml
- template: /pipelines/variables/my-platform-1/common-vars.yml
- template: /pipelines/variables/my-platform-1/microservice1/common-vars.yml
- name: FuncVariablesTemplate
value: /pipelines/variables/my-platform-1/microservice1/app-config-vars.yml
- name: SolPath
value: $(AppSrcRootPath)/$(AppNamespace).sln
- name: mainFuncProjPath
value: $(AppSrcRootPath)/$(AppNamespace)/$(AppNamespace).csproj
- name: ApiCommand
value: $(Build.SourcesDirectory)/testing/my-platform-1/postman/API/collections/microservice1.func-postman_collection.json -e $(Build.SourcesDirectory)/testing/my-platform-1/postman/API/environments/microservice1.func-postman_environment.json --env-var baseUrl=https://$(FunctionAppName).azurewebsites.net/api -r htmlextra --reporters cli,junit,htmlextra --reporter-htmlextra-export $(System.DefaultWorkingDirectory)/Results/PostmantTest.html --reporter-htmlextra-browserTitle "Postman Test Summary"
- name: ApimCommand
value: $(Build.SourcesDirectory)/testing/my-platform-1/postman/APIM/collections/microservice1.apim-postman_collection.json -e $(Build.SourcesDirectory)/testing/my-platform-1/postman/APIM/environments/microservice1.apim-postman_environment.json --env-var baseUrl=https://myorg-apim-$(EnvShortCode)-ne.azure-api.net/my-platform-1/microservice1/ --env-var ClientId=$(ClientId) --env-var TenantId=$(TenantId) --env-var Scope=$(Scope) --env-var SubscriptionKey=$(APIMSubscriptionKey) --env-var ClientSecret=$(ADClientSecret) -r htmlextra --reporters cli,junit,htmlextra --reporter-htmlextra-export $(System.DefaultWorkingDirectory)/Results/PostmantTest.html --reporter-htmlextra-browserTitle "Postman Test Summary"
trigger:
branches:
include:
- main
paths:
include:
- src/my-platform-1/microservice1
stages:
- stage: CI
displayName: Function CI
jobs:
- template: ../build/templates/function-dotnet--ci-template.yml
parameters:
buildConfiguration: ${{ parameters.buildConfiguration }}
appName: $(AppName)
appNamespace: $(AppNamespace)
appSrcRootPath: $(AppSrcRootPath)
solPath: $(SolPath)
mainFuncProjPath: $(mainFuncProjPath)
sonarProjectKey: $(SonarProjectKey)
sonarProjectName: $(SonarProjectName)
archiveName: $(AppName).zip
- ${{ if eq(parameters.RunApiTests, true) }}:
- template: ../build/templates/publish-postman-ci-template.yml
- stage: DEV
displayName: Deploy to DEV
condition: and(succeeded('CI'), ne(variables['Build.Reason'], 'PullRequest'))
variables:
- template: /pipelines/variables/common-vars-dev.yml
- template: /pipelines/variables/my-platform-1/dev-vars.yml
- template: /pipelines/variables/my-platform-1/microservice1/dev-vars.yml
- group: KeyVault-my-platform-1-Dev
- template: ${{ variables.FuncVariablesTemplate }}
jobs:
- template: ../release/templates/functions-cd-job-template.yml
parameters:
AzureSubscriptionConnection: ${{ variables.AzureSubscriptionConnection }}
Environment: ${{ variables.DevOpsEnvName }}
RunApiTests: ${{ parameters.RunApiTests }}
RunApimTests: ${{ parameters.RunApimTests }}
ApiCommand : ${{ variables.ApiCommand }}
ApimCommand : ${{ variables.ApimCommand }}
SetAdditionalSettings: true
# DeployToSlots: false
- stage: QA
displayName: Deploy to QA
condition: and(succeeded('CI'), ne(variables['Build.Reason'], 'PullRequest'))
variables:
- template: /pipelines/variables/common-vars-qa.yml
- template: /pipelines/variables/my-platform-1/qa-vars.yml
- template: /pipelines/variables/my-platform-1/microservice1/qa-vars.yml
- group: KeyVault-my-platform-1-QA
- template: ${{ variables.FuncVariablesTemplate }}
jobs:
- template: ../release/templates/functions-cd-job-template.yml
parameters:
AzureSubscriptionConnection: ${{ variables.AzureSubscriptionConnection }}
Environment: ${{ variables.DevOpsEnvName }}
RunApiTests: ${{ parameters.RunApiTests }}
RunApimTests: ${{ parameters.RunApimTests }}
SetAdditionalSettings: true
# DeployToSlots: false
- stage: PROD
displayName: Deploy to PROD
condition: and(succeeded('CI'), ne(variables['Build.Reason'], 'PullRequest'))
variables:
- template: /pipelines/variables/common-vars-prod.yml
- template: /pipelines/variables/my-platform-1/prod-vars.yml
- template: /pipelines/variables/my-platform-1/microservice1/prod-vars.yml
- group: KeyVault-my-platform-1-PROD
- template: ${{ variables.FuncVariablesTemplate }}
jobs:
- template: ../release/templates/functions-cd-job-template.yml
parameters:
AzureSubscriptionConnection: ${{ variables.AzureSubscriptionConnection }}
Environment: ${{ variables.DevOpsEnvName }}
RunApiTests: ${{ parameters.RunApiTests }}
RunApimTests: ${{ parameters.RunApimTests }}
SetAdditionalSettings: true
# DeployToSlots: false
.NET Azure Function CI (build) Pipeline TEMPLATE
This builds and runs the unit tests. It also includes a SonarCloud code scan and publish step and Whitesource Bolt open-source library checks. There is also a step that ensures all the Nugets have been consolidated to the same version.
This template has all the necessary parameters to be used across all projects.
parameters:
- name: buildConfiguration
displayName: Build Configuration
default: Release
type: string
values:
- Release
- Debug
- name: appName
type: string
- name: appNameSpace
type: string
- name: appSrcRootPath
type: string
- name: solPath
type: string
- name: mainFuncProjPath
type: string
- name: sonarProjectKey
type: string
- name: sonarProjectName
type: string
- name: archiveName
type: string
- name: zipUsingPublishTask
displayName: Zip Using Publish Task
default: true
type: boolean
jobs:
- job: Build
displayName: Build
pool:
vmImage: ubuntu-latest
steps:
- task: SonarCloudPrepare@1
displayName: Prepare for SonarCloud Scan
inputs:
SonarCloud: $(SonarCloudAccount)
organization: $(SonarCloudOrganisation)
scannerMode: 'MSBuild'
projectKey: $(SonarCloudProjectKey)
projectName: $(SonarCloudProjectName)
extraProperties: |
sonar.projectBaseDir=$(Build.SourcesDirectory)
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/coverage/coverage.opencover.xml
# sonar.exclusions=${{parameters.envSonarExclusions}}
# Check that all Nuget Packages are Consolidated
- task: DotNetCoreCLI@2
displayName: Install dotnet-consolidate
inputs:
command: 'custom'
custom: 'tool'
arguments: 'install dotnet-consolidate --global --version 2.0.0'
- task: DotNetCoreCLI@2
displayName: Ensure Consolidated Packages
inputs:
command: 'custom'
custom: 'consolidate'
arguments: '-s $(solPath)'
- task: DotNetCoreCLI@2
displayName: Build Solution
inputs:
projects: $(solPath)
arguments: '--configuration ${{ parameters.buildConfiguration }}'
- task: DotNetCoreCLI@2
displayName: Run Unit Tests
inputs:
command: test
projects: $(solPath)
publishTestResults: true
arguments: '--configuration ${{ parameters.buildConfiguration }} /p:CollectCoverage=true /p:MergeWith=$(Build.SourcesDirectory)/coverage/coverage.json /p:CoverletOutput=$(Build.SourcesDirectory)/coverage/ "/p:CoverletOutputFormat=\"json,opencover,Cobertura\""'
nobuild: true
- task: SonarCloudAnalyze@1
displayName: Sonar Cloud Analysis
- task: PublishCodeCoverageResults@1
displayName: Publish Code Coverage
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: $(Build.SourcesDirectory)/coverage/coverage.cobertura.xml
failIfCoverageEmpty: false
- task: SonarCloudPublish@1
displayName: Sonar Cloud - Publish Result
inputs:
pollingTimeoutSec: '300'
- task: DotNetCoreCLI@2
displayName: Publish API Function App
inputs:
command: publish
arguments: --configuration ${{ parameters.buildConfiguration }} --output $(build.artifactstagingdirectory)/${{ parameters.appName }}
projects: $(mainFuncProjPath)
publishWebProjects: false
modifyOutputPath: false
zipAfterPublish: ${{ parameters.zipUsingPublishTask }}
- task: ArchiveFiles@2
displayName: "Archive Files"
condition: and(succeeded(), eq(${{ parameters.zipUsingPublishTask }}, false))
inputs:
rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/${{ parameters.appName }}'
includeRootFolder: false
archiveType: 'zip'
archiveFile: '$(Build.ArtifactStagingDirectory)/${{ parameters.appName }}/${{ parameters.archiveName }}'
replaceExistingArchive: true
- task: WhiteSource@21
displayName: White Source Security Scan
inputs:
cwd: '${{ parameters.appSrcRootPath }}'
projectName: ${{ parameters.appName }}
# An error should be a warning rather than a fail
continueOnError: true
- publish: $(Build.ArtifactStagingDirectory)/${{ parameters.appName }}/${{ parameters.archiveName }}
displayName: Publish Build Artifact
artifact: ${{ parameters.appName }}
.NET Azure Function CD (Deployment) Pipeline TEMPLATE
This template is used to deploy an Azure Function App. This same template can be used to deploy to all environments, just by changing the parameter values. There are some extra options to run Postman Collection API Tests using Newton either directly against the Function or via the API Manager. This can be used to verify the staging slot directly, prior to doing a swap-slot. If the test fails, then you don’t continue with making the latest changes live. If the direct tests succeed, then you can swap-slot and verify it’s still good through the API manager.
# Job Template for creating or updating an Api
parameters:
- name: AzureSubscriptionConnection
displayName: Azure Subscription Connection
type: string
- name: Environment
displayName: The environment in Azure DevOps
type: string
- name: RunApiTests
displayName: Run the API Tests
type: boolean
default: true
- name: RunApimTests
displayName: Run the APIM Tests
type: boolean
default: true
- name: SetAdditionalSettings
displayName: Set additional app settings
type: boolean
default: false
- name: DeployToSlots
displayName: Deploy Function App to Slots
type: boolean
default: true
- name: ApiCommand
displayName: API Test Command
type: string
default: ' '
- name: ApimCommand
displayName: API Test Command
type: string
default: ' '
jobs:
- deployment: Deploy_Function_App
displayName: Deploy Function App
pool:
vmImage: ubuntu-latest
environment: ${{ parameters.Environment }}
strategy:
runOnce:
deploy:
steps:
- ${{ if eq(parameters.DeployToSlots, true) }}:
- task: AzureFunctionApp@1
displayName: API Funcion App Deployment - $(AppNamespace)
inputs:
azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
appType: 'functionApp'
appName: '$(FunctionAppName)'
deployToSlotOrASE: true
resourceGroupName: '$(FunctionAppResourceGroupName)'
slotName: 'staging'
package: '$(Pipeline.Workspace)/$(AppName)/$(AppName).zip'
appSettings: $(appSettings)
deploymentMethod: 'auto'
- ${{ if eq(parameters.DeployToSlots, false) }}:
- task: AzureFunctionApp@1
displayName: API Funcion App Deployment - $(AppNamespace)
inputs:
azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
appType: 'functionApp'
appName: $(FunctionAppName)
resourceGroupName: $(FunctionAppResourceGroupName)
package: $(Pipeline.Workspace)/$(AppName)/$(AppName).zip
appSettings: $(appSettings)
deploymentMethod: 'auto'
- ${{ if eq(parameters.SetAdditionalSettings, true) }}:
- task: AzureAppServiceSettings@1
displayName: Additional Slot Settings
inputs:
azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
appName: '$(FunctionAppName)'
resourceGroupName: '$(FunctionAppResourceGroupName)'
slotName: 'staging'
appSettings: $(additionalAppSettings)
connectionStrings: $(additionalConnSettings)
generalSettings: $(additionalGeneralSettings)
- ${{ if eq(parameters.DeployToSlots, true) }}:
- task: AzureAppServiceManage@0
displayName: 'Swap staging slot for ${{ parameters.Environment }}'
inputs:
azureSubscription: ${{ parameters.AzureSubscriptionConnection }}
Action: 'Swap Slots'
WebAppName: '$(FunctionAppName)'
ResourceGroupName: '$(FunctionAppResourceGroupName)'
SourceSlot: 'staging'
PreserveVnet: true
- job: APIPostmanTesting
displayName : 'Api Postman Testing on ${{ parameters.Environment }}'
condition: and(succeeded(), eq('${{ parameters.RunApiTests }}', 'true'))
dependsOn: Deploy_Function_App
pool:
vmImage: 'ubuntu-latest'
steps:
- template: /pipelines/build/templates/postman-runner-ci-template.yml
parameters:
SourcePath : ${{ parameters.ApiCommand }}
- job: APIMPostmanTesting
displayName : 'APIM Postman Testing on ${{ parameters.Environment }}'
condition: and(succeeded(), eq('${{ parameters.RunApimTests }}', 'true'))
dependsOn: Deploy_Function_App
pool:
vmImage: 'ubuntu-latest'
steps:
- template: /pipelines/build/templates/postman-runner-ci-template.yml
parameters:
SourcePath : ${{ parameters.ApimCommand }}
Postman API Test Publish Pipeline
This job just assembles the test assets to make them accessible for the deployment stages
# Job Template for packaging the Postman Test assets for pipelines
jobs:
- job: PostmanBuild
dependsOn: Build
condition: ne(variables['Build.Reason'], 'PullRequest')
displayName: Publish Postman Artifacts
pool:
vmImage: ubuntu-latest
steps:
- task: CopyFiles@2
inputs:
SourceFolder: 'testing/$(PlatformShortCode)/postman'
Contents: '**'
TargetFolder: '$(Build.ArtifactStagingDirectory)/${{ parameters.appName }}/postman'
- publish: $(Build.ArtifactStagingDirectory)/${{ parameters.appName }}/postman
displayName: Publish Build Artifact
artifact: postman
Postman API Test Pipeline
For completeness I have included the Postman Tests template
parameters:
- name: SourcePath
displayName: Source path for the postman collection
type: string
steps:
- checkout: self
- task: CmdLine@2
- script: |
npm install -g newman
npm install -g newman-reporter-junitfull
npm install -g newman-reporter-htmlextra
workingDirectory: '$(System.DefaultWorkingDirectory)'
displayName: 'Installing Newman'
- task: CmdLine@2
- script: 'newman run ${{ parameters.SourcePath }}'
workingDirectory: '$(System.DefaultWorkingDirectory)'
displayName: Running the Postman API Testing $(AppName) - $(EnvShortCode)
continueOnError: true
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '$(System.DefaultWorkingDirectory)/Results/test.html'
failTaskOnFailedTests: true
testRunTitle: API $(AppName) - $(EnvShortCode) - Postman test results
displayName: API $(AppName) - Postman test results
- task: UploadPostmanHtmlReport@1
displayName: Postman HTML Report For $(AppName) - $(EnvShortCode)
inputs:
cwd: '$(System.DefaultWorkingDirectory)/Results'
tabName: 'Postman'