Managing Business Central Development with Git: Branching Strategy

The last few posts have been about manipulating the history of your Git repository, getting comfortable tools like rebase, reset, cherry-pick and commit –amend. That’s all geared towards trying to create a history which is more than just a record of stuff that happened but tells a story of the development of your app that is useful for your colleagues and your future self.

This post is on the same theme but we’re talking about your branching strategy. Remember one of the strengths of Git is how easy it is to create branches to isolate pieces of development from each other. That’s an awesome tool – but how do we make best use of it?

When is it useful to separate pieces of development from each other in different branches? How and when do you stick the pieces of the jigsaw back together again?

Options

As you’d expect there are a lot of different approaches and no shortage of people online supporting each one. Here are some popular options. I won’t attempt to critique them because we haven’t tried them all and because you can read, try them out for yourselves and form your own opinions.

Git Flow

https://nvie.com/posts/a-successful-git-branching-model/

This approach has a “develop” branch alongside master and feature branches which are used to manage the work in progress before they are merged back to master only when they are ready to be released.

GitHub Flow

https://guides.github.com/introduction/flow/

As with Git Flow, work in progress changes are isolated in their own branches. Unlike Git Flow they are merged directly back into master once they have been reviewed and are ready to go.

Trunk Based Development

https://trunkbaseddevelopment.com/

The key idea is to avoid having long-lived branches other than the trunk (master) branch. Development can be done against other branches but only to facilitate code review and discussion. Changes should be committed to master at least every 24 hours.

Considerations

As before adopting any tool or practice we need to think about our particular circumstances and needs. What are we actually trying to achieve? By all means read about what other people are doing. If you keep reading I’ll share what we’re (currently) doing but you should think about your own requirements, decide on something that makes sense for you and be prepared to improve it in future.

I think there is something to learn from each of the strategies I’ve linked to.

App Development

We are developing apps for Business Central either to be deployed via AppSource or installed through our partners on-premise to their customers. Either way, making a new version of our app available to our customers is not a trivial exercise.

When we submit a new version of our app it is typically at least 3 or 4 working days until it is available in AppSource. For on-prem customers we are reliant on our partners to upgrade the apps manually. Neither of these scenarios exactly falls into the ideal “continuous deployment” category. Some branching strategies are geared towards getting code into master as soon as possible so it can be pushed to the production environment each day, or even multiple times a day.

However attractive that might sound that is just isn’t reality for us – at least not yet. We’re due to be getting an API for pushing updates to AppSource, which is great, but as long as it is backed by a manual certification process I can’t see Microsoft thanking us for pushing multiple updates each day.

Given the lead time to getting a release live we should be quite careful about what is going to go into each one. We don’t really have the luxury of pushing an update immediately after another because we forgot to include something.

#1 Create a Release Branch

We start by creating a release branch. This is where we are going to collect all the changes that should be included in the next release before they are merged into the master branch. We do occasionally bundle in last minute changes and fixes to a release but we ought to have a pretty clear idea of what the release will include before we start.

Imagine we’ve got this repo. All of the commits are merged into the master branch which is tagged with 1.0.0. Tags are useful additional pointers to particular places in the history of the repo. In future if we want to see the code as it was in v1.0.0 we can just run git checkout 1.0.0

* 3894d1a (HEAD -> master, tag: 1.0.0, origin/master) Correct typo in caption
* cd03362 Add missing caption for new field
* 94388de Populate new Customer field OnInsert
* c49b9c9 Add new field to Customer card

Now create a new branch to use as our release branch. For now this just points to the same commit as master.

git branch release/1.1.0

#2 Create Individual Feature and Bug Branches

Now we’ll create separate branches for each feature or bug fix that we’ve decided to include in release 1.1.0. Why not just do all the changes we need in the release branch? Because we want to be able to develop and test them separately from each other.

* 381c83d (HEAD -> bug/commission-calc) Fix rounding error in commission calc
| * e9d31b4 (feature/sales-report) Action to open sales report from customer
| * 78102dd Sales report
|/
| * c450814 (feature/sales-price-calc) Prices in non-base UOM
| * dd5f6c0 Prices in additional currencies
| * 02fa619 Pricing elements per item
|/
* 3894d1a (tag: 1.0.0, origin/master, release/1.1.0, master) Correct typo in caption
* cd03362 Add missing caption for new field
* 94388de Populate new Customer field OnInsert
* c49b9c9 Add new field to Customer card

The graph might look something like this now. Separate branches with one or more commits in each. Incidentally, naming the branches feature/* and bug/* is just a convention – it doesn’t have any affect on how they are managed.

#3 Create Pull Requests and Complete Quickly

When each feature or bug fix is ready for review and testing we create a pull request targeting the release branch. Pull requests in Azure DevOps are great. However, in my experience there are two main things that make pull requests less great, or even bad.

  1. Bundling too many changes in a single pull request
  2. Leaving them open for too long

Having lots of changes makes it difficult to review and test those changes. Which means no one is enthusiastic to do it. Which means it gets left open for a long time.

Leaving pull requests open for a long time means people forget what the changes were for and whether they have already been tested. It becomes a burden that no one wants to take responsibility for. Eventually someone completes it because we’re all sick of seeing it on the list. Not an ideal reason to complete it.

We’ve got a couple of measures on our team dashboard – number of open pull requests and average age of those requests in days. If the average age is creeping over 7, say, then we’re likely doing something wrong.

We squash the commits when the pull request is completed. Like it sounds, that squashes all of the changes that are in the feature or bug branch into a single commit which is added to the release branch. We lose some of the history doing this but I think it makes it more readable later on. We are rarely interested in the details of how we wrote a certain feature – just that we did, and these were the changes that we made.

* 35cf673 (HEAD -> release/1.1.0) Merged PR 03: Commission Calc
* b23b8c5 Merged PR 02: Sales Report
* 8007dcf Merged PR 01: Sales Price Calc
| * 381c83d (bug/commission-calc) Fix rounding error in commission calc
|/
| * e9d31b4 (feature/sales-report) Action to open sales report from customer
| * 78102dd Sales report
|/
| * c450814 (feature/sales-price-calc) Prices in non-base UOM
| * dd5f6c0 Prices in additional currencies
| * 02fa619 Pricing elements per item
|/
* 3894d1a (tag: 1.0.0, origin/master, master) Correct typo in caption
* cd03362 Add missing caption for new field
* 94388de Populate new Customer field OnInsert
* c49b9c9 Add new field to Customer card

Here is the graph now. I’ve removed the remote branches to keep it simpler. Notice the “Merged PR” commits which have been created by completing the pull requests. I’ve still got local branches with the individual changes. These can now safely be deleted now that those changes have been squashed into the release branch.

#4 Merge into Master and Tag

Each push to the server triggers a pipeline to compile the code and run the tests. Assuming those builds are passing and with the manual testing that we’ve done we ought to be confident that the changes work as expected. Each time we complete a pull request it runs a build incorporating the other completed changes. If that passes as well then we’re ready to merge the changes into master, delete the release branch and tag the new version as 1.1.0

* 35cf673 (HEAD -> master, origin/master, tag: 1.1.0) Merged PR 03: Commission Calc
* b23b8c5 Merged PR 02: Sales Report
* 8007dcf Merged PR 01: Sales Price Calc
* 3894d1a (tag: 1.0.0) Correct typo in caption
* cd03362 Add missing caption for new field
* 94388de Populate new Customer field OnInsert
* c49b9c9 Add new field to Customer card

The end result – at least what we’re aiming for – is a neat summary of the changes that have been made between the two versions. We can see the changes which we made for each feature or bug fix in those commits. If we want more detail we can always go back and view the completed pull request on Azure DevOps.

In a future post we’ll think about how to manage different versions of the code for different versions of Business Central.

Further Reading

Check out Michael Megel’s post on the same topic here: https://never-stop-learning.de/branching-workflow-ci-cd-part-6/

Scheduling Azure DevOps Pipelines with YAML

I had the pleasure of presenting some thoughts about developing apps for SaaS with James Crowter to the Dutch Dynamics Community yesterday. We were sharing some of our experiences of the maintenance challenge that comes with having published apps on AppSource.

How can you continuously test your apps against past, current and upcoming versions of Business Central? Perhaps two ways:

  1. Slowly drive yourself to despair with the monotony of creating different versions of Business Central environments and testing manually
  2. Automate as much of the tedious infrastructure and repetitive testing work as possible so you can concentrate on some fun stuff instead

We have two main reasons to trigger the execution of the pipeline for a given branch of an app in Azure DevOps:

  1. We have changed some code
  2. Microsoft have changed some code that we depend on

If we have changed some of our own code we should run it through the pipeline to ensure that it passes our checks, the automated tests run and that the resulting .app files are versioned and signed correctly. It is easy to overlook some of these tasks and/or inadvertently break some existing functionality when making our changes. The pipeline is there to have our back.

At the same time, Microsoft are making changes to the base and system applications that we rely on. Even if we don’t have any planned changes for our apps we may need to make some code changes to accommodate what Microsoft have done to the ground underneath our feet.

With a bit of luck we’ll see this sort of thing:

warning AL0432: Method 'FilterReservFor' is marked for removal. Reason: Replaced by ProdOrderLine.SetReservationFilters(FilterReservEntry)

warning AL0432: Method 'CreateReservEntryFor' is marked for removal. Reason: Replaced by CreateReservEntryFor(ForType, ForSubtype, ForID, ForBatchName, ForProdOrderLine, ForRefNo, ForQtyPerUOM, Quantity, QuantityBase, ForReservEntry)

We’re using a method that Microsoft are making obsolete and will be removed at some point in the future. No need to panic, but be aware that you should switch to the new method. Very civilised. Thanks.

With less luck we’ll find that Microsoft have introduced a change that breaks our app in some way – with a compilation error or unintended behaviour. Either way, it’s something that we want to know about.

Scheduling pipelines can help with that.

Typically we:

  • Develop against a W1 version of the latest sandbox image, run pipelines against our latest commits against mcr.microsoft.com/businesscentral/sandbox with a continuous integration trigger
  • Migrate changes backwards to BC14 and BC13 compatible versions of our apps, run pipelines against appropriate Docker images for those versions
  • Have separate branches which we rebase onto the latest commit to run pipelines against bcinsider.azurecr.io/bcsandbox and bcinsider.azurecr.io/bcsandbox-master with a schedule

The continuous integration trigger is straightforward enough. At the top of our .azure-pipelines.yml we have:

trigger:
  - '*'

The schedule is defined in a separate section of the yml file, like this:

schedules:
  - cron: 0 3 * * Sun
    displayName: Schedule insider builds
    branches:
      include: ['build/insider', 'build/insider-master']
    always: true

Those branches are the ones that are set to build against the insider Docker images. I hadn’t come across cron before, but it’s pretty simple. The schedule is defined as:

  • Minute
  • Hour
  • Day of month
  • Month
  • Day of week

Our schedule comes out as 03:00 every Sunday. Asterisks stand for any value. https://crontab.guru/ is useful for getting your head around the format.

The branches key defines which branches are included in the schedule and the always indicates that we always want to run the pipeline, even if there haven’t been any code changes since it was last run.

Using Templates in YAML Pipelines in Azure DevOps

So far we’ve been considering how you can define a yaml pipeline to define the steps required to build the code in a single repository. Create a .azure-pipelines.yml file, add the stages, jobs and steps and away you go. Cool.

What if you’re building multiple apps with the source code in multiple repositories though? You could just copy your pipeline definition from repo to repo. What happens when you want to make changes to the pipeline? Are you going to copy the changes here, there and everywhere?

No. You’ve got more self-respect than that. You want a single pipeline definition that is shared across the repos that need it. In which case, templates will be of interest.

Create a Template File

If you’ve got a yaml pipeline definition that already works for you you’re probably going to want to use that as the basis of your template. Copy and paste your pipeline into a new yaml file. You’ll probably want to create a new project or repo to hold this template file.

Remove Trigger

If you’ve got a trigger section in the pipeline you’re copying from (to trigger the pipeline when changes are pushed to certain branches) you can remove that from the template file.

Convert Variables to Parameters

If you have any variables in the pipeline you will need to convert them to parameters. Use the parameters keyword…simple enough. Notice that you can still provide default values for the parameters. If parameters values are not supplied by the pipeline that is using the template these default values will be used. For example:

parameters:
  image_name: mcr.microsoft.com/businesscentral/sandbox
  container_name: Build
  company_name: My Company
  user_name: admin
  password: P@ssword1

Any references to variables in the steps will need to be changed to refer to the parameters instead. Rather than this:

-task: PowerShell@1
  displayName: Create build container
  inputs:
    scriptType: inlineScript
    inlineScript: >
      Import-Module navcontainerhelper;
      New-NavContainer -containerName $(container_name)...

Use ${{parameters.[parameter_name]}} like this:

 -task: PowerShell@1
  displayName: Create build container
  inputs:
    scriptType: inlineScript
    inlineScript: >
      Import-Module navcontainerhelper;
      New-NavContainer -containerName ${{parameters.container_name}}...

I’ve called my template file build-template.yml and the first few lines look like this:

 parameters:
  image_name: mcr.microsoft.com/businesscentral/sandbox
  container_name: Build
  company_name: My Company
  user_name: admin
  password: P@ssword1
  license_file: C:\Users\james.pearson\Desktop\Licence.flf

stages:
- stage: build
  displayName: Build
  jobs:
  - job: Build
    pool:
      name: Default
    steps:
      - task: PowerShell@1    
        displayName: Create build container
        inputs:
          scriptType: inlineScript
          inlineScript: > 
            Import-Module navcontainerhelper;
            $Credential = [PSCredential]::new('${{parameters.user_name}}',(ConvertTo-SecureString '${{parameters.password}}' -AsPlainText -Force));
            ...

Change the Pipeline to Use the Template

Now you want to change the pipeline definition to use the template yaml file that you have created. Include a repository resource, specifying the name with repository key.

The type key refers to the host of the git repo. Confusingly, ‘git’ refers to an Azure DevOps project or you can also refer to templates in GitHub repos. Name is in the format Project/Repository – in my example both are called ‘Templates’. Define a ref (generally a branch or tag) in the template repo that specifies the version of the template you want.

trigger:
  - '*'

resources:
  repositories:
    - repository: templates
      type: git
      name: Templates/Templates
      ref: refs/heads/master

stages:
- template: build-template.yml@templates
  parameters:
    image_name: mcr.microsoft.com/businesscentral/sandbox
    company_name: My Company 

Templates can be used at different levels in the pipeline to specify stages, jobs, steps or variables – see here for more info. In my example the template file is specifying stages to use in the pipeline.

My pipeline simply becomes a template key beneath the stages key. The value is in the format [filename]@[repository]. The repository value here is taken from the repository key specified above. Supply parameter values with the parameters key. Any parameter values that are not supplied will take the default values from the template file.

And there you have it. A single template file that you can reuse across your different repos. Make changes to your pipeline once and have them used wherever the template is used.

YAML Multi-Stage Pipelines in Azure DevOps, Stage 2

In the previous post I introduced you to multi-stage YAML pipelines. Build/Release pipelines vs. a multi-stage pipeline, enabling the preview feature (it’s still in preview at the time of writing) and an overview of the structure of the file.

Now we’ll take a more detailed look at an example multi-stage YAML file. This is geared at building apps for Business Central but the principles are transferable to any other application you are targeting. This is the example that I talked through in my recent webinar. If you’d rather view it as a gist you can see that here.

trigger:
- '*'

pool:
  name: Default

variables:
  image_name: mcr.microsoft.com/businesscentral/sandbox
  container_name: Build
  company_name: My Company
  user_name: admin
  password: P@ssword1
  license_file: C:\Users\james.pearson.TECMAN\Desktop\Licence.flf

stages:
- stage: build
  displayName: Build
  jobs:
  - job: Build
    pool:
      name: Default
    steps:
      - task: PowerShell@1    
        displayName: Create build container
        inputs:
          scriptType: inlineScript
          inlineScript: > 
            Import-Module navcontainerhelper;
            $Credential = [PSCredential]::new('$(user_name)',(ConvertTo-SecureString '$(password)' -AsPlainText -Force));
            New-NavContainer -accept_eula -accept_outdated -containerName '$(container_name)' -auth NavUserPassword -credential $Credential -image $(image_name) -licenseFile $(license_file) -doNotExportObjectsToText -restart no -shortcuts None -useBestContainerOS -includeTestToolkit -includeTestLibrariesOnly -updateHosts
      - task: PowerShell@1
        displayName: Copy source into container folder
        inputs:
          scriptType: inlineScript
          inlineScript: >
            $SourceDir = 'C:\ProgramData\NavContainerHelper\Extensions\$(container_name)\Source';
            New-Item $SourceDir -ItemType Directory;
            Copy-Item '$(Build.SourcesDirectory)\*' $SourceDir -Recurse -Force;
      - task: PowerShell@1
        displayName: Compile app
        inputs:
          scriptType: inlineScript
          inlineScript: >
            Import-Module navcontainerhelper;
            $SourceDir = 'C:\ProgramData\NavContainerHelper\Extensions\$(container_name)\Source';
            $Credential = [PSCredential]::new('$(user_name)',(ConvertTo-SecureString '$(password)' -AsPlainText -Force));
            Compile-AppInNavContainer -containerName '$(container_name)' -appProjectFolder $SourceDir -credential $Credential -AzureDevOps -FailOn 'error';
      - task: PowerShell@1
        displayName: Copy app into build artifacts staging folder
        inputs:
          scriptType: inlineScript
          inlineScript: >
            $SourceDir = 'C:\ProgramData\NavContainerHelper\Extensions\$(container_name)\Source';        
            Copy-Item "$SourceDir\output\*.app" '$(Build.ArtifactStagingDirectory)'
      - task: PowerShell@1
        displayName: Publish and install app into container
        inputs:
          scriptType: inlineScript
          inlineScript: >
            Import-Module navcontainerhelper;        
            Get-ChildItem '$(Build.ArtifactStagingDirectory)' | % {Publish-NavContainerApp '$(container_name)' -appFile $_.FullName -skipVerification -sync -install}
      - task: PowerShell@1
        displayName: Run tests
        inputs:
          scriptType: inlineScript
          inlineScript: >
            $Credential = [PSCredential]::new('$(user_name)',(ConvertTo-SecureString '$(password)' -AsPlainText -Force));
            $BuildHelperPath = 'C:\ProgramData\NavContainerHelper\Extensions\$(container_name)\My\BuildHelper.app';
            Download-File 'https://github.com/CleverDynamics/al-build-helper/raw/master/Clever%20Dynamics_Build%20Helper_BC14.app' $BuildHelperPath;
            Publish-NavContainerApp $(container_name) -appFile $BuildHelperPath -sync -install;
            $Url = "http://{0}:7047/NAV/WS/{1}/Codeunit/AutomatedTestMgt" -f (Get-NavContainerIpAddress -containerName '$(container_name)'), '$(company_name)';
            $AutomatedTestMgt = New-WebServiceProxy -Uri $Url -Credential $Credential;
            $AutomatedTestMgt.GetTests('DEFAULT',50100,50199);
            Import-Module navcontainerhelper;
            $ResultPath = 'C:\ProgramData\NavContainerHelper\Extensions\$(container_name)\my\Results.xml';        
            Run-TestsInBcContainer -containerName '$(container_name)' -companyName '$(company_name)' -credential $Credential -detailed -AzureDevOps warning -XUnitResultFileName $ResultPath -debugMode
      - task: PublishTestResults@2
        displayName: Upload test results    
        inputs:
          failTaskOnFailedTests: true
          testResultsFormat: XUnit
          testResultsFiles: '*.xml'
          searchFolder: C:\ProgramData\NavContainerHelper\Extensions\$(container_name)\my

      - task: PublishBuildArtifacts@1
        displayName: Publish build artifacts
        inputs:
          ArtifactName: App Package
          PathtoPublish: $(Build.ArtifactStagingDirectory)

      - task: PowerShell@1
        displayName: Remove build container
        inputs:
          scriptType: inlineScript
          inlineScript: >
            Import-Module navcontainerhelper;
            Remove-NavContainer $(container_name)
        condition: always()
- stage: Release
  displayName: Release
  condition: and(succeeded(), eq(variables['build.sourceBranch'], 'refs/heads/master'))
  jobs:
  - deployment:
    displayName: Release
    pool:
      name: Default
    environment: Release
    strategy:
      runOnce:
        deploy:
          steps:               
            - task: PowerShell@1
              displayName: Copy artifacts to release directory                  
              inputs:
                scriptType: inlineScript
                inlineScript: >
                  $Path = Split-Path '$(System.ArtifactsDirectory)' -Parent;
                  $Artifact = "$Path\App Package\*.app";
                  Copy-Item $Artifact 'C:\Release\';

Build

I’ve got a simple Business Central app. I want to use the Build stage to take my source AL code and:

  • Compile it into an .app file
  • Publish and install it into a container
  • Load the test codeunits and methods into the DEFAULT test suite
  • Run the tests
  • Upload the test results to the build
  • Upload the .app file as a build artifact

Almost all of these steps are performed with a PowerShell task. I won’t talk you through the PowerShell, you can read the code in the steps and read more about the use of the navcontainerhelper module on Freddy’s blog or dig around the PowerShell posts on my blog. I will just mention that loading the test codeunits relies on our AL Build Helper app as described previously.

The final step to run is a PowerShell script to remove the container that has been created for the build. I always want this step to run, even if another step above it has failed. See the condition: always() line that takes care of that.

Release

Space, Rocket, Travel, Science, Sky, Abstract, Planet

So far this is fairly familiar territory and stuff that we’ve been through before. The more interesting part for the purposes of this post is having a second stage to run.

First notice the condition attached to the Release stage:

and(succeeded(), eq(variables['build.sourceBranch'], 'refs/heads/master'))

build.SourceBranch is one of the built-in variables that you can access in the pipeline which holds the name of the branch that triggered the build. This condition means the Release stage will only run if the pipeline has succeeded up to this point and the pipeline was triggered by the master branch.

This is useful for a CI/CD scenario where you want to trigger the pipeline for changes to any branch (and why would you not?) but only want to Release the code when it is merged back into the master branch.

My pipeline uses a specific type of deployment job which allows you to target a particular environment. The ‘Environments’ menu item is displayed when you enable the multi-stage pipelines preview feature. Including a deployment job in your pipeline will instruct Azure DevOps to automatically download the artifacts from the Build stage to the agent (see here).

The Release stage can include as many steps as you need depending on your definition of ‘releasing’ your software. It might include steps to:

  • Use PowerShell to publish the .app file to an on-prem instance of Business Central
  • Use the admin API to publish the .app file to a Business Central SaaS tenant
  • Upload the .app file to some other location: FTP, SharePoint, network path

As a very simple example my Release stage is simply going to copy the .app file that was downloaded as an artifact from the Build stage into a C:\Release folder on the build agent.

Environments

One of the good things about creating an environment and targeting it with a deployment job is that you can see at a glance which version of your software the environment is hosting.

Drill down on the environment to see details of the current and previous versions that have been deployed and all the relevant corresponding detail – the pipeline that deployed the software, the logs, commits and work items. Beautiful.

YAML Multi-Stage Pipelines in Azure DevOps, Stage 1

Let’s return to the subject of pipelines and this time let’s talk multi-stages. What is it and why might you want to implement it in your YAML file?

Builds/Releases

With the approach that Microsoft are now calling “classic” pipelines there was a definite division between a build pipeline and a release pipeline.

A build starts with a given version of your source code (a particular commit in your git repository, say) and proceeds to define the steps that should be performed on that code to “build” it.

You decide what “build” means and define the steps as you need them. In a Business Central AL extension context we’re probably talking: compile the extension into an app file, publish and install and run some tests.

A release takes artifact(s) that have been created by a particular build and/or code from a particular repository and “releases” them. Again, you define whatever “release” actually means to you. Publish an app file into a Business Central database, upload it to SharePoint, decompress the app file and send the source code to a printer – whatever you want.

Build pipelines can still be defined in the classic, visual editor or in a YAML file. The Azure DevOps interface makes it pretty obvious which way they recommend you do this. It took me a second to spot the discrete “use the classic editor” link when creating a new pipeline.

Clicking that link and successfully avoiding the top option from the following page which still creates a YAML file anyway will get you to the classic, visual editor. Select the agent that is going to run this pipeline, add one or more jobs and add one or more tasks to each job.

Even now, you’ll notice a “View YAML” link in the top right hand corner of the screen. Subtle. The term “classic” usually means something different when we’re talking about software than when we’re talking about novels. Less “masterpiece, will still be appreciated a century from now” and more “outdated, will be made obsolete and removed a few months from now”.

It’s probably a safe bet that the “classic” editor is going to go the same way as NAV’s “classic client” with its “classic reports”.

For completeness, the Release editor looks like this:

I’ve defined the build pipeline that provides the artifacts that will be released and can now define the stages and tasks involved in releasing it. At the time of writing you will still get this editor when creating a new pipelines from the Releases menu.

Multi-Stage Pipelines

Enter multi-stage pipelines. Rather than defining your build and your release tasks in separate editors you can define them in a single YAML pipeline definition.

You’ll need to enable the preview feature (from your profile menu in the top right hand corner). You’ll notice that the “Builds” option disappears from the Pipelines menu and is replaced with two new options “Pipelines” and “Environments”. Intriguing.

Now we can work with pipelines that look something like this:

trigger:
  '*'
parameters:
  image_name: a.docker.image
  container_name: my_container
stages:
- stage: build
  jobs:
  - job: Build
    pool:
      name: Default
    steps:
      (definition of the steps that are included in the build stage)
- stage: release
  condition:  and(succeeded(), eq(variables['build.sourceBranch'], 'refs/heads/master'))
  jobs:
  - deployment:
    pool:
      name: Default
    environment: QA
    (further definition of the steps involved in the release stage)

We’ll go into the details of a complete multi-stage YAML pipeline in another post. For now I just want to outline the structure of the file:

stages (1) -> stage (1..*) -> jobs (1) -> job (1..*) -> steps/deployment/tasks

You can include as many stages as you need to effectively manage the build and deployment of your software. Each stage can evaluate a condition expression which decides whether the stage should be run or not. In my case I only want to run the release stage if the pipeline has succeeded up to this point and the pipeline has been triggered by the master branch.

By default, stages will be run in series and will be dependent upon the previous stage. You can mix this up by defining the dependsOn key for each stage.

Environments

You’ll also notice that my ‘deployment’ task includes the environment to which I’m going to deploy my software. This will correspond to an environment that you have created from the Environments option of the Pipelines menu in Azure DevOps.

You can control how and when code is released to a given environment with stage conditions (as above) or with manual approvals.

Open the environment and select ‘Checks’ from the menu. All approvers that are entered on the following page must approve a pipeline before the deployment to the environment will proceed. The pipeline will be paused in the meantime.

Next time…

I hope that’s enough to whet your appetite to go and investigate the possibilities for yourself and see if/how you can start making use of this in your own development team.

Next time we’ll go through a more complete example of a multi-stage YAML pipeline and how it is put together and works. Until then you might like to check out the recording of the webinar that I did for Areopa webinars. If you like it, do them a favour and subscribe to the channel, thanks.