Some time ago I created a private Azure DevOps Extension for a customer which would create an Azure DevOps Bug WorkItem when a task in a Release would fail.
When returning at that customer this Extension was still used but it didn’t work for (yaml) build pipelines. Instead of updating this Extension I investigated if I was able to use Azure DevOps Service Hooks to trigger an Azure Logic App which would create a Bug WorkItem for failed (build) Yaml Pipelines.
After sharing a successful test implementation result on Twitter it seemed that some more people are interested in how I got this working.
So here is that blog post :-)
For those unfamiliar with Logic Apps, it is a cloud service that helps you schedule, automate, and orchestrate tasks, business processes, and workflows when you need to integrate apps, data, systems, and services across enterprises or organizations.
Logic Apps simplifies how you design and build scalable solutions for app integration, data integration, system integration, enterprise application integration (EAI), and business-to-business (B2B) communication, whether in the cloud, on premises, or both. You can find more information here.
For creating the Azure DevOps Bug WorkItem we need a trigger, which fires when a specific event (like a failed task in a build pipeline) happens. Each time that the trigger fires, the Logic Apps engine creates a logic app instance that runs the actions in the workflow.
For our Logic App we are going to use a Response Request trigger.
This Trigger will be run by the Azure DevOps Service Hooks Web Hook.
Web Hooks provides a way to send a JSON representation of an event to any service. All that is required is a public endpoint (HTTP or HTTPS).
Later in this blog post I’ll explain the configuration needed here to trigger the Logic App. Let’s now first start with the creation of the Logic App.
Go to the Azure Portal and create a new Logic App Resource
You should now have an empty Logic App Resource created.
Let’s add our first Action “When a HTTP request is received” by opening the Designer.
At a new POST Method Parameter and save the Logic App and copy the HTTP Get URL.
We need to use this URL in the Azure DevOps Web Hook later.
Open Azure DevOps Project Settings and create a new Web Hook using the URL from previous step.
Click on next after selecting Web Hooks.
We want for each of the completed Builds with Status Failed to have the Logic App being triggered.
Configure the properties of the Web Hook and test the webhook.
Copy the Request from the Test to the Logic App to be used as sample payload to generate schema.
Click on Done and save Logic App.
The following Actions need to be added.
Save Logic App.
Save Logic App.
Expression:
replace(split(triggerBody()?['resource']?['url'],'.')[0],'https://','')
Explanation:
From the “When a HTTP request is received” output we want to extract the Organization name and store that value in a variable.
Save Logic App.
With this action we want to retrieve the current Azure DevOps Iteration. More info on this REST API can be found here.
You first need to create a connection to Azure DevOps.
Configure the Send an HTTP Request to Azure DevOps without configuring the Relative URL.
Go to Code View after first saving the Logic App.
https://dev.azure.com/@{variables('Organization')}/@{triggerOutputs()['queries']['Project']}/@{triggerOutputs()['queries']['Team']}/_apis/work/teamsettings?api-version=5.1
Explanation:
The URL should look something like this:
https://dev.azure.com/stefanstranger/Contoso/Contoso Team/_apis/work/teamsettings?api-version=5.1
Expression | Value | Explanation |
---|---|---|
@{variables(‘Organization’)} | stefanstranger | This value is from the earlier variable |
@{triggerOutputs()[‘queries’][‘Project’]} | Contoso | This value is coming from a parameter input on the Web Hook Webrequest. Example: https://foo.com?Project=Contoso |
@{triggerOutputs()[‘queries’][‘Team’]} | Contoso Team | This value is coming from a parameter input on the Web Hook Webrequest. Example: https://foo.com?Team=Contoso%20Team |
Save Logic App.
Use the following Dynamic content. We want to use the DefaultIteration path from the previous Action.
"@body('Send_an_HTTP_request_to_Azure_DevOps_-_Get_Iteration')['DefaultIteration']['path']"
Go to the Designer add temp value and save Action before going to View Code.
Save Logic App.
Add a new Azure DevOps Action to your Logic App.
Configure the following properties:
Save Action and open Code View Editor and replace with the following:
"Create_a_work_item": {
"inputs": {
"body": {
"area": "@{triggerOutputs()['queries']['Area']}",
"description": "@{triggerBody()?['detailedMessage'].text}",
"dynamicFields": {
"Microsoft.VSTS.TCM.ReproSteps": "@{triggerBody()?['detailedMessage'].html}"
},
"iteration": "@{triggerOutputs()['queries']['Area']}@{variables('Iteration')}",
"title": "@{triggerBody()?['message']?['text']} - @{triggerBody()?['resource']?['finishTime']}"
},
"host": {
"connection": {
"name": "@parameters('$connections')['visualstudioteamservices']['connectionId']"
}
},
"method": "patch",
"path": "/@{encodeURIComponent('Contoso')}/_apis/wit/workitems/$Bug",
"queries": {
"account": "stefanstranger"
}
},
"runAfter": {
"Set_variable_-_Iteration": [
"Succeeded"
]
},
"type": "ApiConnection"
},
Save Logic App.
We now need to add the following parameter to the web hook url:
Open the configured Web Hook in Azure DevOps and copy the content to Notepad and add the following:
Test the updated Web Hook in both Azure DevOps and in Azure.
If you get an unauthorized error message on the Send an HTTP request to Azure DevOps Action.
Go to API Connections and Delete the Azure DevOps Connection.
Go to the Logic Analytics Designer and re-add the Connection by Adding a new one.
Save the Logic App. And Authorize API Connection again and save.
The final test is when we create a (build) pipeline which fails to validate the Logic App being triggered and the Bug Work Item being created.
yaml pipeline code:
trigger:
- none
steps:
- powershell: |
throw 'Something failed!'
Trigger the pipeline.
Check the Logic App Runs History in the Portal.
And finally check in Azure DevOps if the Bug Work Item is being created.
Here is the complete Code for the Logic App.
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Create_a_work_item": {
"inputs": {
"body": {
"area": "@{triggerOutputs()['queries']['Area']}",
"description": "@{triggerBody()?['detailedMessage'].text}",
"dynamicFields": {
"Microsoft.VSTS.TCM.ReproSteps": "@{triggerBody()?['detailedMessage'].html}"
},
"iteration": "@{triggerOutputs()['queries']['Area']}@{variables('Iteration')}",
"title": "@{triggerBody()?['message']?['text']} - @{triggerBody()?['resource']?['finishTime']}"
},
"host": {
"connection": {
"name": "@parameters('$connections')['visualstudioteamservices']['connectionId']"
}
},
"method": "patch",
"path": "/@{encodeURIComponent('Contoso')}/_apis/wit/workitems/$Bug",
"queries": {
"account": "contosodemo"
}
},
"runAfter": {
"Set_variable_-_Iteration": [
"Succeeded"
]
},
"type": "ApiConnection"
},
"Initialize_variable_-_Iteration": {
"inputs": {
"variables": [
{
"name": "Iteration",
"type": "string"
}
]
},
"runAfter": {},
"type": "InitializeVariable"
},
"Initialize_variable_-_Organization": {
"inputs": {
"variables": [
{
"name": "Organization",
"type": "string"
}
]
},
"runAfter": {
"Initialize_variable_-_Iteration": [
"Succeeded"
]
},
"type": "InitializeVariable"
},
"Send_an_HTTP_request_to_Azure_DevOps_-_Get_Iteration": {
"inputs": {
"body": {
"Method": "GET",
"Uri": "https://dev.azure.com/@{variables('Organization')}/@{triggerOutputs()['queries']['Project']}/@{triggerOutputs()['queries']['Team']}/_apis/work/teamsettings?api-version=5.1"
},
"host": {
"connection": {
"name": "@parameters('$connections')['visualstudioteamservices']['connectionId']"
}
},
"method": "post",
"path": "/httprequest",
"queries": {
"account": "contosodemo"
}
},
"runAfter": {
"Set_variable_-_Organization": [
"Succeeded"
]
},
"type": "ApiConnection"
},
"Set_variable_-_Iteration": {
"inputs": {
"name": "Iteration",
"value": "@body('Send_an_HTTP_request_to_Azure_DevOps_-_Get_Iteration')['DefaultIteration']['path']"
},
"runAfter": {
"Send_an_HTTP_request_to_Azure_DevOps_-_Get_Iteration": [
"Succeeded"
]
},
"type": "SetVariable"
},
"Set_variable_-_Organization": {
"inputs": {
"name": "Organization",
"value": "@{replace(split(triggerBody()?['resource']?['url'],'.')[0],'https://','')}"
},
"runAfter": {
"Initialize_variable_-_Organization": [
"Succeeded"
]
},
"type": "SetVariable"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
}
},
"triggers": {
"manual": {
"inputs": {
"method": "POST",
"schema": {
"properties": {
"createdDate": {
"type": "string"
},
"detailedMessage": {
"properties": {
"html": {
"type": "string"
},
"markdown": {
"type": "string"
},
"text": {
"type": "string"
}
},
"type": "object"
},
"eventType": {
"type": "string"
},
"id": {
"type": "string"
},
"message": {
"properties": {
"html": {
"type": "string"
},
"markdown": {
"type": "string"
},
"text": {
"type": "string"
}
},
"type": "object"
},
"notificationId": {
"type": "integer"
},
"publisherId": {
"type": "string"
},
"resource": {
"properties": {
"buildNumber": {
"type": "string"
},
"definition": {
"properties": {
"batchSize": {
"type": "integer"
},
"definitionType": {
"type": "string"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"triggerType": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
},
"drop": {
"properties": {
"downloadUrl": {
"type": "string"
},
"location": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
},
"dropLocation": {
"type": "string"
},
"finishTime": {
"type": "string"
},
"hasDiagnostics": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"lastChangedBy": {
"properties": {
"displayName": {
"type": "string"
},
"id": {
"type": "string"
},
"imageUrl": {
"type": "string"
},
"uniqueName": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
},
"log": {
"properties": {
"downloadUrl": {
"type": "string"
},
"type": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
},
"queue": {
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"queueType": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
},
"reason": {
"type": "string"
},
"requests": {
"items": {
"properties": {
"id": {
"type": "integer"
},
"requestedFor": {
"properties": {
"displayName": {
"type": "string"
},
"id": {
"type": "string"
},
"imageUrl": {
"type": "string"
},
"uniqueName": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
},
"url": {
"type": "string"
}
},
"required": [
"id",
"url",
"requestedFor"
],
"type": "object"
},
"type": "array"
},
"retainIndefinitely": {
"type": "boolean"
},
"sourceGetVersion": {
"type": "string"
},
"startTime": {
"type": "string"
},
"status": {
"type": "string"
},
"uri": {
"type": "string"
},
"url": {
"type": "string"
}
},
"type": "object"
},
"resourceContainers": {
"properties": {
"account": {
"properties": {
"id": {
"type": "string"
}
},
"type": "object"
},
"collection": {
"properties": {
"id": {
"type": "string"
}
},
"type": "object"
},
"project": {
"properties": {
"id": {
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
},
"resourceVersion": {
"type": "string"
},
"subscriptionId": {
"type": "string"
}
},
"type": "object"
}
},
"kind": "Http",
"type": "Request"
}
}
},
"parameters": {
"$connections": {
"value": {
"visualstudioteamservices": {
"connectionId": "/subscriptions/13bd0bca-5bf3-4c8b-be25-3661251884b8/resourceGroups/bug-workitem-demo-rg/providers/Microsoft.Web/connections/visualstudioteamservices",
"connectionName": "visualstudioteamservices",
"id": "/subscriptions/13bd0bca-5bf3-4c8b-be25-3661251884b8/providers/Microsoft.Web/locations/westeurope/managedApis/visualstudioteamservices"
}
}
}
}
}