Azure DevOps - Auto Approve PR with PAT
I hear a lot of people ask, how can I problematically approve a PR when using Azure DevOps? See the example script below.
You will need a valid PAT, Azure DevOps Org URL, Azure DevOps Project Name/ID.
You will have to make small adjustment to the code to work, so please see comments inline.
You will have to make small adjustment to the code to work, so please see comments inline.
import argparse
import json
import requests
# Go ahead and add all the users you want to approve with a PAT
# This is the easiest way, otherwise we would have to run a lot of
# api requests to figure out the user identity based on the current
# PAT used
TEAM = [
'user1@example.com',
'user2@example.com',
]
def submit_to_api(pat=None, url=None, data=None, method="get", return_type='json', json=None):
result = None
if method == 'get':
query_parameters = data
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
if query_parameters:
result = requests.get(url,
params=query_parameters, headers=headers, auth=('', pat))
else:
result = requests.get(url, headers=headers, auth=('', pat))
elif method == 'delete':
json_dictionary = data
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
result = requests.delete(url, data=json_dictionary,
headers=headers, auth=('', pat))
elif method == 'put':
json_dictionary = data
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
result = requests.put(url, data=json_dictionary,
headers=headers, auth=('', pat))
elif method == 'patch':
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
if data:
json_dictionary = data
result = requests.patch(url, data=json_dictionary,
headers=headers, auth=('', pat))
elif json:
json_dictionary = json
result = requests.patch(url, json=json_dictionary,
headers=headers, auth=('', pat))
else:
json_dictionary = data
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
result = requests.post(url, data=json_dictionary,
headers=headers, auth=('', pat))
json = None
if int(result.status_code) >= 400:
msg = 'Submit Response = ' + \
str(result.status_code) + "\n" + result.text
raise Exception(msg)
if return_type == 'json':
try:
json = result.json()
except Exception:
raise Exception("Can't parse Json Respone\n" + json.text)
else:
try:
json = result.text
except Exception:
raise Exception("Can't get text\n" + json)
return json
def get_user_local_id(user, pat, module=None):
# my original code was hard coded to our org, you can just refactor to pass this in or
# update org to your visual studio org name
org = 'my_vs_project_name'
identity_url = f'https://{org}.visualstudio.com/_apis/IdentityPicker/Identities?api-version=5.0-preview.1'
identity_obj = """
{
"query":"{0}",
"identityTypes":["user","group"],
"operationScopes":["ims","source"],
"options":{"MinResults":5,"MaxResults":40},
"properties":[
"DisplayName",
"IsMru",
"ScopeName",
"SamAccountName",
"Active",
"SubjectDescriptor",
"Department",
"JobTitle",
"Mail",
"MailNickname",
"PhysicalDeliveryOfficeName",
"SignInAddress",
"Surname",
"Guest",
"TelephoneNumber",
"Manager",
"Description"
]
}
""".replace("{0}", user)
local_id = None
json = submit_to_api(pat=pat, url=identity_url, data=identity_obj, method="post")
if json:
local_id = json['results'][0]['identities'][0]['localId']
return local_id
def update_approval(pat, org, project, repo_id, pr_id, reviewer_id, module=None):
url = f'{org}/{project}/_apis/git/repositories/{repo_id}/pullRequests/{pr_id}/reviewers/{reviewer_id}?api-version=6.0-preview'
reviewer_obj = '{ "vote": "10"}'
update_json = None
json = submit_to_api(pat=pat, url=url,
data=reviewer_obj, method="put")
if json:
update_json = json
return update_json
def get_pr_title(pat, org, project, repo_id, pr_id, module=None):
url = f'{org}/{project}/_apis/git/repositories/{repo_id}/pullrequests/{pr_id}?api-version=6.0-preview.1'
update_json = None
description = None
json = submit_to_api(pat=pat, url=url,
data=None, method="get")
if json:
update_json = json
description = update_json['title']
return description
def update_auto_complete(pat, org, project, repo_id, pr_id, reviewer_id, module=None):
url = f'{org}/{project}/_apis/git/repositories/{repo_id}/pullrequests/{pr_id}?api-version=6.0-preview'
title = get_pr_title(pat, org, project, repo_id, pr_id)
update_json = None
if title:
# You might want to change reviewer_obj particularly mergeCommitMessage
reviewer_obj = """
{
"completionOptions":
{
"autoCompleteIgnoreConfigIds":[],
"bypassPolicy":false,
"bypassReason":"",
"deleteSourceBranch":true,
"mergeCommitMessage":"Auto-Merged PR {pr}: {title}",
"mergeStrategy":2,
"transitionWorkItems":false
},
"autoCompleteSetBy":
{
"id": "{id}"
}
}
""".replace("{id}", reviewer_id).replace("{pr}", pr_id).replace("{title}", title)
json = submit_to_api(pat=pat, url=url,
data=reviewer_obj, method="patch")
if json:
update_json = json
return update_json
def approve_pull_request(pat, pr_ids, org, project, repo_id, module=None):
results = []
for pr in pr_ids:
for name in TEAM:
if module:
module.warn(str(name))
reviewer_id = get_user_local_id(name, pat, module=module)
if module:
module.warn(str(reviewer_id))
result = None
try:
result = update_approval(pat, org, project, repo_id, pr, reviewer_id, module=module)
except Exception:
pass
if result:
result2 = update_auto_complete(pat, org, project, repo_id, pr, reviewer_id, module=module)
results.append(result)
return results
def main():
parser = argparse.ArgumentParser(
description='Auto approve Pull Request(s)')
parser.add_argument('--pat', type=str, nargs='?', required=True,
help='vsts Pat')
parser.add_argument('--pr_id', type=str, nargs='+', required=True,
help='Pull Request id or list of Pull Request ids')
parser.add_argument('--org', type=str, default='https://dev.azure.com/org_id',
help='Azure DevOps Org URL')
parser.add_argument('--repo_id', type=str, default='repo_id',
help='Azure DevOps Repo Id')
parser.add_argument('--project', type=str, default='project_id',
help='Azure DevOps Project Name/ID')
args = parser.parse_args()
approve_pull_request(args.pat, args.pr_id, args.org, args.project, args.repo_id)
if __name__ == '__main__':
main()
Assuming you saved the file to auto_approve_pull_request.py you can run the code like below. You can then pass in the parameters as desired.
python3 ./auto_approve_pull_request.py
## Output
# usage: auto_approve_pull_request.py [-h] --pat [PAT] --pr_id PR_ID [PR_ID ...] [--org ORG] [--repo_id REPO_ID] [--project PROJECT]
# auto_approve_pull_request.py: error: the following arguments are required: --pat, --pr_id
# you can use this to get help
python3 ./auto_approve_pull_request.py --help
# Output
#usage: auto_approve_pull_request.py [-h] --pat [PAT] --pr_id PR_ID [PR_ID ...] [--org ORG] [--repo_id REPO_ID] [--project PROJECT]
#
#Auto approve Pull Request(s)
#
#optional arguments:
# -h, --help show this help message and exit
# --pat [PAT] vsts Pat
# --pr_id PR_ID [PR_ID ...]
# Pull Request id or list of Pull Request ids
# --org ORG Azure DevOps Org URL
# --repo_id REPO_ID Repo Id
# --project PROJECT Azure DevOps Project Name/ID
# You will likely call the script like this:
python3 ./auto_approve_pull_request.py --pat 'some_guid' --pr_id 1234 --org 'https://dev.azure.com/my_org' --repo_id 'My_repo_id' --project 'My_Project_Name'
I recommend saving the PAT to keyvault and using a lookup to pass to your script. If you find this useful, please comment, share and let me know how you decide to integrate.
Comments
Post a Comment
Comments with irrelevant links will be deleted.