Crafting complex Release Gate conditions in VSTS

2018-04-03

A recent addition to VSTS is the ability to run a quick check prior to triggering a release to an environment. These checks can be used to check that there are no new customer complaints, no mad users on twitter, no important jobs running in the environment etc.

I wanted to make use of this feature as part of the CI/CD Tasks for Extensions. With the recent introduction of validation of the extension, it's possible that your newly uploaded extension isn't immediately available. So before kicking off tests for your extension, you may want to wait for the validation to succeed.

This resulted in two tasks, the first is a Server Gate task that can wait for the extension validation to finish before triggering a release. The second does the same thing, but runs on the build agent. They both rely on the Marketplace REST API.


To check the status of an extension, one can call the Marketplace REST API. It will return a list of versions of the extension and their validation status:

GET https://marketplace.visualstudio.com/_apis/gallery/publishers/jessehouwing/extensions/vsts-snyk?flags=1
Response Code: OK
Response: 
{
     "publisher":{
         "publisherId":"c68591c6-8fbd-413b-b7fb-b921737f4f9f",
         "publisherName":"jessehouwing",
         "displayName":"Jesse Houwing",
         "flags":"verified"
     },
     "extensionId":"252ad2b4-a2c5-43fc-bba5-17e631896091",
     "extensionName":"vsts-snyk",
     "displayName":"Snyk Task",
     "flags":"validated, public",
     "versions":[
         {"version":"1.1.12","flags":"validated","lastUpdated":"2018-03-16T18:58:53.133Z"},
         {"version":"1.1.11","flags":"validated","lastUpdated":"2018-02-18T13:53:47.83Z"},
         {"version":"1.1.9","flags":"validated","lastUpdated":"2017-03-14T09:40:40.75Z"}
     ],
     "deploymentType":0
}

To ensure the latest version is valid, you can use jsonpath to retrieve this information from the reply:

eq(
  count(
    jsonpath(
      '$.versions[?(@.version==''1.1.12'' && @.flags==''validated'')]'
    )
  ), 
  1
)

To configure the call and the expression, you need to go into your release pipeline and enable Gates on the environment. Then add an Invoke REST API  gate and configure it:



If the API call requires authentication, you can supply these through a basic credential.

Once you've configured your gate you can test it by triggering a release. It took me a while to get the expression right and after triggering 8 releases I'd reached a critical frustration level. Before using the Invoke REST API release gate I had been testing my expressions through publishing an extension and that ook even longer. I decided to build a small tool to quickly test the release conditions locally, without having to queue a build.




As you can see, the expression tester allows you to test an expression and provides direct feedback. I've added support for simple Build and Release variables as well, they're automatically detected when you type the expression.

With this little tool, your time to create and validate complex expressions will drop from multiple minutes per iteration to multiple iterations per minute. The expression parser used by the server tries to do a couple of clever things and short-circuits the validation of expressions. This caused me to detect issues in the second jsonpath only after triggering the second condition ($(extensionVersion) = 'latest').

Once you have a working Release Gate, it's easy to take the configuration and turn it into a reusable Task that can be published as part of an extension:

{
  "id": "231decda-22cb-4e83-b2f4-31fc86a0de1f",
  "name": "Check Marketplace validation status.",
  "category": "Deploy",
  "runsOn": [
    "Server",
    "ServerGate"
  ],
  "inputs": [
    {
      "name": "connectedServiceName",
      "type": "connectedService:VstsMarketplacePublishing",
      "label": "VSTS Marketplace connection",
      "required": true,
    },
    {
      "name": "publisherId",
      "type": "string",
      "label": "Publisher ID",
    },
    {
      "name": "extensionId",
      "type": "string",
      "label": "Extension ID",
    },
    {
      "name": "extensionVersion",
      "type": "string",
      "label": "Extension Version",
    }
  ],
  "execution": {
    "HttpRequest": {
      "Execute": {
        "EndpointId": "$(connectedServiceName)",
        "EndpointUrl": "$(endpoint.url)_apis/gallery/publishers/$(publisherId)/extensions/$(extensionId)?flags=1",
        "Method": "GET",
        "Body": "",
        "Headers": "{\"Content-Type\":\"application/json\"}",
        "Expression": "or(eq(count(jsonpath('$.versions[?(@.version==''$(extensionVersion)'' && @.flags==''validated'')]')), 1), and(in('$(extensionVersion)', 'latest', ''), eq(jsonpath('$.versions[0].flags')[0], 'validated')))"
      }
    }
  }
}

After publishing this task as an extension it shows up in the Release Gate section and will ask for a couple of variables you're probably already familiar with:

If you've configured your BuildNumber to be just the version you want to publish (default for my pipelines) and made the extension your primary artefact, then you can pass in the $(Build.BuildNumber) variable to wait for that specific version.


The tool I created to test the expression is available as a download on GitHub. The tool requires a local installation of Team Foundation Server 2018 Update 2 RC (due to the fact that I can't distribute the server binaries).

To help discover how to build and the release gates, I used the following examples:
 

Most Reading