Search code examples
azure-devopsazure-pipelinesazure-pipelines-yaml

Pre-deploy approvals in Azure Devops YAML pipeline


The company I'm at is investigating whether we can migrate from classic Azure DevOps Builds and Releases towards YAML Pipelines. Using these pipelines we want to deploy to four four different environments (dev, test, acc, prod). On a regular basis we need to validate a feature that has not made it into the main branch, i.e. we deploy a feature branch to dev and/or test. Whenever the main branch is updated, the latest version of main is automatically deployed to dev and test, which is problematic when we're in the process of testing the feature branch. Our solution with the classic pipelines is to temporary add a pre-deploy approval.

YAML pipelines do not have pre- and post-approvals, instead one has to add an approval to an environment. This has been asked and answered on SO. In our company, this approach has a major drawback: We have 10-20 pipelines that continuously deploy to dev and test, and by adding an approval to the dev environment for the sake of testing a single application, we halt ALL deployments to dev. Manually approving the the deployments of applications that do not interfere with the ongoing test activities is tedious at best.

Recently I discovered the task ManualValidation@0, which might present a solution to the aforementioned problem, but it too comes with some challenges. The manual validation should on default be skipped, only to be activated when some environmental variable or variable from a Library is set. Also, the step has to be added to each YAML pipeline making them bloated. Before continuing on that path, I wonder whether others have come up with a better solution.

---------------------------- Solution ----------------------------

Based on the recommendations by @Alvin Zhao - MSFT, I came to a solution that I'd like to share.

The company I'm at has several websites that are deployed in (practically) the same way, i.e. the same steps are taken. It is therefore worth investing in a Git repository that contains the shared steps. Here is an excerpt of the deploy template:

#file deploy-website.yml

parameters:
# some params omitted for brevity
- name: preDeployApproversVariable #POI 1
  type: string
  default: ''
- name: postDeployApprovalVariable
  type: string
  default: ''

jobs: 
 - ${{ if ne(parameters.preDeployApproversVariable, '')}}: #POI 2
   - template: wait-for-manual-approval.yml
     parameters
        jobName: AwaitPreDeployApproval
        approvers: ${{parameters.preDeployApproversVariable}}
        instructions: Approve or reject deploying 
        dependsOn: #POI 3

 - deployment: WebDeployment
  ${{ if ne(parameters.preDeployApproversVariable,'') }} #POI 4
    dependsOn: [AwaitPreDeployApproval]
  strategy:
    runOnce:
      #The actual deployment steps are listed here

 - ${{ if ne(parameters.postDeployApprovalVariable, '')}}:
   - template: wait-for-manual-approval.yml
     parameters:
       jobName: AwaitPostDeployApproval
       approvers: ${{parameters.postDeployApprovalVariable}}
       instructions: Approve or reject deploying 
       dependsOn: 
        - WebDeployment

There are some several points of interestest (POI):

  • POI 1: Parameters preDeployApproversVariable and postDeployApproversVariable are parameters containing the name of a variable retrieved from a variable group, they must not contain the value.
  • POI 2: If the caller of this template, e.g. the pipeline, does not list any pre- or post-deploy parameters, the wait-for-manual-approval.yml templates does not even appear in the compiled template.
  • POI 3: The template wait-for-manual-approval.yml (see code below) has a mandatory parameter dependsOn, which for the pre-approval can be left empty, for the post-approval it's probably better to add a value.
  • POI 4: The job 'WebDeployment' has a conditional dependsOn on the pre-approval job.
#File: wait-for-manual-approval.yml 
parameters:
- name: jobName #POI 5
  type: string
- name: approvers
  type: string
- name: instructions
  type: string
  default: 'Approve or reject'
- name: dependsOn
  type: object

jobs: 
- job: '${{paramters.jobName}}'
  ${{ if parameters.dependsOn}}: #POI 6
    dependsOn: '${{parameters.dependsOn}}'
  pool: server
  timeoutInMinutes: 60
  steps:
  - task: ManualValidation@0
    condition: ne(variables['${{parameters.approvers}}'], '') #POI 7
    inputs:
     notifyUsers: | 
       $(${{parameters.approvers}})  #POI 8
     instructions: ${{parameters.instructions}}
    onTimeout: reject

The points of interest are:

  • POI 5: The jobname is an input parameter to the template such that the template can be used multiple times within the same stage.
  • POI 6: Conditional dependency, this is needed to ensure that the post-deploy approval does not start before the deployment has completed.
  • POI 7: The parameter approvers is expanded at compile time and must therefore be the name of a variable that comes from e.g. a variable group. If the value of this variable in the variable group is left empty, the condition is not met and the ManualValidation@0 is skipped.
  • POI 8: Using macro-syntax to get the value of the variable from the variable group.

Finally, the pipeline looks like this:

#omitted
resources:
  repositories:
    - repository: CommonTemplates
      type: git
      name: Git_repo_name

stage: Development
jobs: 
  - template: deploy-website.yml@CommonTemplates
    parameters:
      preDeployApprovers: PreDeployApproverApp1
      postDeployApprovers: PostDeployApproverApp1

Solution

  • Update

    Based on the discussions, the workaround to add the condition property like eq(variables['Build.SourceBranchName'], 'feature') or eq(variables['PreDeployApprovalRequired'], 'true') for the ManualValidation@1 task can help skip approval checking according to the branch from which the source code is consumed, or the variable value set in the variable group.


    Based on the current descriptions, it doesn't seem necessary to add approval checks for those environment resources, but you may try adding approval checks for a variable group resource.

    Taking the YAML pipeline below for example, it will reference the variable group Feature Deployment on the condition ${{ if eq(variables['Build.SourceBranchName'], 'feature') }}, and will keep waiting for the approval to use this variable group.

    trigger:
    - main
    - feature
    
    variables:
    - ${{ if eq(variables['Build.SourceBranchName'], 'feature') }}:
      - group: Development
    - ${{ if eq(variables['Build.SourceBranchName'], 'main') }}:
      - group: MainDevelopment
    
    steps:
    - checkout: self
    - script: |
        echo $(PreDeployApprovalRequired)
    

    enter image description here enter image description here

    With that being said, it still requires you to create another variable group for the YAML definition from main branch to reference to avoid pending approval when deploying the code from main branch.

    See more information on Pipeline deployment approvals - Azure Pipelines | Microsoft Learn.