I have always wanted to learn a bit of Python, so I set out to do a project in Django where I have a form that I can fill out and when submitted it will start an Azure DevOps pipeline and provide the data from the form as parameters.
Working with customers using ServiceNow, Jira, etc., it can be a useful tool when trying to automate tasks in Azure. Maybe you want to fully automate your landing zone deployment using a service request form with approvals, or you want to allow developers to be able to order a VM or public IP?
The order of things
The only reliable way of getting this to work for me has been by configuring the different resources in a specific order.
- Create a service connection of type incoming webhook
- Create pipeline – do not use an existing one, instead copy everything from an existing to a new
- Configure the webhook resource in the pipeline
- Assign the service connection permissions to the pipeline
If you get an error it will most likely state that it can’t find a webhook with the given name.
Create incoming webhook
Let’s start by creating the incoming webhook.
- In Azure DevOps, go to Project Settings -> Service connections and select New service connection
- Scroll down and select Incoming WebHook
- Fill in the required information:
- Webhook name: Used in pipeline and also when triggering the pipeline using a POST request
- Secret: Something secure, we don’t want everyone to be able to use our webhook
- Http header: Anything you want
- Service connection name: Something descriptive, will also be used in the pipeline
- Security: Untick Grant access permissions to all pipelines
Create pipeline
Next up is to create the pipeline and configure the webhook resource inside it.
- Go to Pipelines -> New pipeline -> Azure Repos Git
- Select your repository
- Select Starter pipeline
The below pipeline code will disable other triggers to make sure we only trigger the pipeline using our webhook.
The resource section references our webhook name and service connection name.
We can also see that two parameters are referenced. They’re picked up from the payload sent with the POST request when triggering the webhook. More on that later!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Disable non-webhook triggers
trigger: none
pr: none
# Webhook resource that triggers pipeline
resources:
webhooks:
- webhook: WebookDemoTrigger ### Webhook name
connection: WebhookDemoConnection ### Incoming webhook service connection name
pool:
vmImage: ubuntu-latest
steps:
- task: PowerShell@2
inputs:
targetType: inline
script: |
# Write parameters from webhook payload to console
Write-Host ${{ parameters.WebookDemoTrigger.app_name }}
Write-Host ${{ parameters.WebookDemoTrigger.environment }}
Assign permissions to the pipeline
We’re done with steps 1-3, but since we unticked the box that gives the service connection permissions to all pipelines, we need to make sure we give it access to our pipeline.
Go to your service connection (the incoming webhook), select the three dots and Security.
Under Pipeline permissions, select the + sign and add your newly created pipeline.
Fire in the hole!
We should now be able to trigger the webhook using our preferred method. I’ll provide examples for PowerShell and Python.
PowerShell (stolen from GitHub, credit to Igor Abade)
The uri requires us to fill in our Azure DevOps organization name, and the name of the incoming webhook (WebhookDemoTrigger).
In $Body we can define the data sent in the payload, this is what we can later reference using:
${{ parameters.. }}
“HMAC can be used to determine whether a message sent over an insecure channel has been tampered with, provided that the sender and receiver share a secret key. The sender computes the hash value for the original data and sends both the original data and hash value as a single message. The receiver recalculates the hash value on the received message and checks that the computed HMAC matches the transmitted HMAC.”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Param (
[uri]
$Url = "https://dev.azure.com/MY_ORG/_apis/public/distributedtask/webhooks/WebookDemoTrigger/?api-version=6.0-preview",
[string]
$Secret = "Demo123!",
[string]
$HeaderName = "MyHeader"
)
$Body = @{
app_name = "my app"
environment = "prod"
} | ConvertTo-Json
$hmacSha = New-Object System.Security.Cryptography.HMACSHA1 -Property @{
Key = [Text.Encoding]::ASCII.GetBytes($secret)
}
$hashBytes = $hmacSha.ComputeHash([Text.Encoding]::UTF8.GetBytes($Body))
$signature = ''
$hashBytes | ForEach-Object { $signature += $_.ToString('x2')}
$headers = @{
$headerName = "sha1=$signature"
}
Invoke-WebRequest -Uri $Url -Body $Body -Method Post -ContentType 'application/json' -Headers $headers
Python
The below code will import the necessary modules, define a function to generate the sha1 signature and a function to fire the webhook.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# Import modules
import hmac
import hashlib
import json
import requests
import logging
# Configure logging format and level
logging.basicConfig(format='%(asctime)s: %(message)s')
logging.getLogger().setLevel(logging.INFO)
# Data payload for webhook
data = {
"app_name": "MyLilApp",
"environment": "Prod",
}
# Generate sha1 signature
def create_sha1_signature(secret, payload):
# Convert payload to json
data = json.dumps(payload)
# Encode payload and store in bytes format
data = data.encode("utf-8")
# Encode secret and store in bytes format
key = secret.encode("utf-8")
# Sign payload using secret and construct sha1 signature
signature = "sha1=" + hmac.new(key, data, hashlib.sha1).hexdigest()
return signature
# Trigger webhook with payload
def trigger_azure_webhook():
# Define variables
secret = "Demo123!"
header_name = "MyHeader"
adops_org = "MY_ORG"
adops_webhook = "WebookDemoTrigger"
endpoint = f"https://dev.azure.com/{adops_org}/_apis/public/distributedtask/webhooks/{adops_webhook}/?api-version=6.0-preview"
# Construct header
headers = {
header_name: create_sha1_signature(secret, data)
}
# Fire webhook
try:
response = requests.post(url=endpoint, json=data, headers=headers)
response.raise_for_status()
if response.status_code == 200:
logging.info("The request was a success!")
except requests.exceptions.HTTPError as errh:
logging.exception(errh)
logging.exception(response.text)
except requests.exceptions.ConnectionError as errc:
logging.exception(errc)
logging.exception(response.text)
except requests.exceptions.Timeout as errt:
logging.exception(errt)
logging.exception(response.text)
except requests.exceptions.RequestException as err:
logging.exception(err)
logging.exception(response.text)
trigger_azure_webhook()
And that’s it! Once fired the pipeline will start and do its job.
Thanks for reading! Feel free to reach out on any of my socials in the bottom.