Skip to content

Commit 0306bec

Browse files
authored
feat: Azure DevOps: merging pull requests with conventional commits (#997)
* feat: Azure DevOps: merging pull requests with conventional commits * feat: image * fix: nits * fix: repetition
1 parent 288676b commit 0306bec

File tree

5 files changed

+316
-8
lines changed

5 files changed

+316
-8
lines changed

blog-website/blog/2025-07-25-azure-devops-api-pull-requests-merge-set-autocomplete/index.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ authors: johnnyreilly
55
tags: [typescript, azure devops, node.js]
66
image: ./title-image.png
77
hide_table_of_contents: false
8-
description: 'How to have merge a pull request in Azure DevOps or set it to autocomplete using the Azure DevOps Client for Node.js.'
8+
description: 'How to merge a pull request in Azure DevOps or set it to autocomplete using the Azure DevOps API Client for Node.js.'
99
---
1010

1111
Have you ever wanted to merge a pull request in Azure DevOps using the Azure DevOps API? Or set a pull request to autocomplete, so it automatically merges when all policies are satisfied? If so, you're in the right place. In this post, I'll show you how to do just that using the Azure DevOps Client for Node.js.
@@ -16,6 +16,8 @@ I'm using the Azure DevOps Client for Node.js; but if you want to use the REST A
1616

1717
To get up and running with the Azure DevOps Client for Node.js, you can [see how we work with it in this post on dynamic required reviewers in Azure DevOps](../2025-06-25-azure-devops-pull-requests-dynamic-required-reviewers/index.md) post. This will help you set up your environment and authenticate with Azure DevOps.
1818

19+
If you'd like to read about setting commit messages when merging pull requests in Azure DevOps, you can check out my post on [merging pull requests with conventional commits in Azure DevOps](../2025-08-29-azure-devops-pull-requests-conventional-commits/index.md).
20+
1921
<!--truncate-->
2022

2123
## Merging a pull request
@@ -45,10 +47,10 @@ async function mergePullRequest({
4547
},
4648
/** repositoryId */ repositoryName,
4749
pullRequest.pullRequestId!,
48-
/** project */ projectName
50+
/** project */ projectName,
4951
);
5052
console.log(
51-
`✅ Successfully merged pull request ${pullRequest.pullRequestId}`
53+
`✅ Successfully merged pull request ${pullRequest.pullRequestId}`,
5254
);
5355
} catch (error) {
5456
const errorMessage = `❌ Failed to merge pull request ${pullRequest.pullRequestId}`;
@@ -89,7 +91,7 @@ async function setPullRequestToAutocomplete({
8991
const { authenticatedUser } = await locationsApi.getConnectionData();
9092

9193
console.log(
92-
`Setting pull request ${pullRequest.pullRequestId} to auto-complete with squash merge as ${authenticatedUser?.providerDisplayName} (${authenticatedUser?.id})`
94+
`Setting pull request ${pullRequest.pullRequestId} to auto-complete with squash merge as ${authenticatedUser?.providerDisplayName} (${authenticatedUser?.id})`,
9395
);
9496

9597
await gitApi.updatePullRequest(
@@ -103,10 +105,10 @@ async function setPullRequestToAutocomplete({
103105
},
104106
/** repositoryId */ repositoryName,
105107
pullRequest.pullRequestId!,
106-
/** project */ projectName
108+
/** project */ projectName,
107109
);
108110
console.log(
109-
`✅ Successfully set pull request ${pullRequest.pullRequestId} to auto-complete`
111+
`✅ Successfully set pull request ${pullRequest.pullRequestId} to auto-complete`,
110112
);
111113
} catch (error) {
112114
const errorMessage = `❌ Failed to set pull request ${pullRequest.pullRequestId} to auto-complete`;
@@ -115,12 +117,12 @@ async function setPullRequestToAutocomplete({
115117
}
116118
```
117119

118-
What might be surprising about this code is that you have explicitly provide your user id when setting the pull request to autocomplete. The unlovely aspect of this is that you need to discover that id somehow. We achieve it here by fetching the authenticated user from the `locationsApi`.
120+
What might be surprising about this code is that you have explicitly provide your user id when setting the pull request to autocomplete. The unlovely aspect of this is that you need to discover that id somehow. We achieve it here by fetching the authenticated user from the `locationsApi`.
119121

120122
Once you have the user id, you can set the `autoCompleteSetBy` property of the pull request to that user id. This will allow the pull request to be set to autocomplete. Again we must specify the `mergeStrategy` so it knows how to merge when the time comes.
121123

122124
## Conclusion
123125

124126
In this post, we've seen how to merge a pull request and set it to autocomplete using the Azure DevOps Client for Node.js. This can be a powerful way to automate your workflow and ensure that pull requests are merged when they meet the necessary criteria.
125127

126-
I'm personally using this in build validation pipelines to ensure that pull requests are merged automatically when all policies are satisfied. This helps to streamline the development process and reduce manual intervention.
128+
I'm personally using this in build validation pipelines to ensure that pull requests are merged automatically when all policies are satisfied. This helps to streamline the development process and reduce manual intervention.
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
---
2+
slug: azure-devops-pull-requests-conventional-commits
3+
title: 'Azure DevOps: merging pull requests with conventional commits'
4+
authors: johnnyreilly
5+
tags: [typescript, azure devops, node.js]
6+
image: ./title-image.png
7+
hide_table_of_contents: false
8+
description: 'How to merge a pull request in Azure DevOps and maintain a git commit history of conventional commits, using the Azure DevOps API and build validations.'
9+
---
10+
11+
There was a time in my life when I didn't really care about commit messages. I would just write whatever I felt like, and it was fine. Over time, I learned that good commit messages are important for understanding the history of a project, especially when working in a team. And also, because I tend to forget what I've been working on surprisingly quickly.
12+
13+
There's also more technical reasons to care about commit messages. For example, if you're using a tool like [semantic-release](https://semantic-release.gitbook.io/semantic-release/) to automate your release process, it relies on conventional commit messages to determine the next version number and generate release notes. It turns out that Azure DevOps has some challenges when it comes to maintaining a git commit history of conventional commits, especially when merging pull requests. By default, Azure DevOps uses a commit strategy that creates a merge commit with a message like "Merge PR 123: Title of pull request". This is acts against conventional commits.
14+
15+
![title image reading "Azure DevOps: merging pull requests with conventional commits" with an Azure DevOps logo](title-image.png)
16+
17+
You can use the UI to change the commit message when completing a pull request, but it's very easy to forget to do this. And if you're using squash merges, you lose the individual commit messages from the feature branch, which can be a problem if you're trying to maintain a history of conventional commits.
18+
19+
There is a way to bend Azure DevOps to our will; to allow us to control our commit messages. In this post, I'll show you how to do just that using the Azure DevOps API, some TypeScript and build validations. The fact this mechanism lives in a build validation means you cannot forget to set the commit message. That's the feature.
20+
21+
This post is not, in fact, specifically about using conventional commits. That's just a common use case. Rather this post is about being able to control the commit message when merging pull requests in Azure DevOps.
22+
23+
<!--truncate-->
24+
25+
## The approach
26+
27+
The internet has been angry about Azure DevOps pull request commit messages for a while. There are [feature requests](https://developercommunity.visualstudio.com/t/change-default-title-for-pull-request-commits-to-n-1/365716) which have been open since 2018 and [Stack Overflow questions](https://stackoverflow.com/questions/55636169/how-to-change-pr-merge-commit-message) on the topic.
28+
29+
Azure DevOps very rarely gets new features these days, and so it's unlikely that we'll see any changes here. However, there is one avenue that is open to us. Azure DevOps has the ability for a pull request to be set to autocomplete, which means that it will automatically merge when all policies are satisfied. This is useful for ensuring that the pull request is merged without manual intervention once it meets the requirements. For example when build validations have passed, and the required reviewers have approved.
30+
31+
I've written about [merging pull requests and setting autocomplete with the Azure DevOps API](../2025-07-25-azure-devops-api-pull-requests-merge-set-autocomplete/index.md) previously. We're going to build on that knowledge here, but add in the magic of setting the merge commit message when we set the pull request to autocomplete. This is achieved by the [pull requests API](https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/update?view=azure-devops-rest-7.1#gitpullrequestcompletionoptions). It allows us to update a pull request and set it autocomplete with a specific message commit message.
32+
33+
This should allow us to go from commits like this:
34+
35+
![Screenshot of the default merge commit in the style Merge PR 123: Title of pull request](screenshot-default-merge-commit.webp)
36+
37+
To commits like this:
38+
39+
![Screenshot of a merge commit with a conventional commit message](screenshot-semantic-merge-commit.webp)
40+
41+
I should say that I'm using conventional commits as my commit message style, but you can use whatever style you like. The important thing is that you have control over the commit message.
42+
43+
## The code
44+
45+
We're going to write a script that can be run in a build validation pipeline. This script will set the pull request to autocomplete with a specific merge commit message. This means that if you use conventional commits, you'll get to maintain a history of conventional commits in your git history.
46+
47+
Before we dive into the full code, here's the bit that does the magic of setting the merge commit message when setting the pull request to autocomplete:
48+
49+
```ts
50+
const updateData = {
51+
autoCompleteSetBy: {
52+
id: authenticatedUser.id,
53+
},
54+
completionOptions: {
55+
mergeStrategy: 'squash',
56+
mergeCommitMessage: 'feat: conventional commit message', // <- set your commit message here
57+
},
58+
};
59+
60+
const response = await fetch(
61+
`https://dev.azure.com/${org}/${project}/_apis/git/repositories/${repo}/pullrequests/${pullRequestId}?api-version=7.1`,
62+
{
63+
method: 'PATCH',
64+
headers: defaultHeaders,
65+
body: JSON.stringify(updateData),
66+
},
67+
);
68+
```
69+
70+
Here we are:
71+
72+
- Setting the `autoCompleteSetBy` property to the authenticated user. This is required when setting a pull request to autocomplete.
73+
- Setting the `completionOptions.mergeStrategy` to `squash`. [You can change this to `rebase` or `noFastForward` if you prefer those strategies.](https://learn.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/update?view=azure-devops-rest-7.1#gitpullrequestmergestrategy)
74+
- Setting the `completionOptions.mergeCommitMessage`. This is where we set our conventional commit message. Or if you wanted to use a different style, you could set it to whatever you like.
75+
76+
### The full TypeScript script
77+
78+
Now that we understand the principle, here's the full `set-autocomplete-and-commit-message.ts` script that you can use in your build validation pipeline:
79+
80+
```ts
81+
import { Buffer } from 'buffer';
82+
import { parseArgs } from 'util';
83+
84+
interface LocationData {
85+
authenticatedUser?: AuthenticatedUser;
86+
}
87+
88+
interface AuthenticatedUser {
89+
customDisplayName?: string;
90+
id: string;
91+
providerDisplayName: string;
92+
}
93+
94+
function getArgs() {
95+
const { values } = parseArgs({
96+
options: {
97+
token: {
98+
type: 'string',
99+
short: 't',
100+
description: 'Personal Access Token for Azure DevOps API',
101+
},
102+
'pr-id': {
103+
type: 'string',
104+
short: 'i',
105+
description: 'Pull Request ID',
106+
},
107+
org: {
108+
type: 'string',
109+
short: 'o',
110+
description: 'Azure DevOps organization name',
111+
},
112+
project: {
113+
type: 'string',
114+
short: 'j',
115+
description: 'Azure DevOps project name',
116+
},
117+
repo: {
118+
type: 'string',
119+
short: 'r',
120+
description: 'Repository name',
121+
},
122+
},
123+
});
124+
125+
const token = values.token;
126+
const currentPullRequestId = values['pr-id'];
127+
const org = values.org
128+
?.replace('https://dev.azure.com/', '')
129+
.replace('/', '');
130+
const project = values.project;
131+
const repo = values.repo;
132+
133+
if (!token) {
134+
throw new Error('PAT token must be provided using --token');
135+
}
136+
if (!currentPullRequestId) {
137+
throw new Error('Pull Request ID must be provided using --pr-id');
138+
}
139+
if (!org) {
140+
throw new Error('Organization must be provided using --org');
141+
}
142+
if (!project) {
143+
throw new Error('Project must be provided using --project');
144+
}
145+
if (!repo) {
146+
throw new Error('Repository must be provided using --repo');
147+
}
148+
149+
return { token, currentPullRequestId, org, project, repo };
150+
}
151+
152+
async function getAuthenticatedUser(
153+
org: string,
154+
defaultHeaders: Record<string, string>,
155+
) {
156+
console.log('Fetching authenticated user info...');
157+
158+
const connectionDataResponse = await fetch(
159+
`https://dev.azure.com/${org}/_apis/ConnectionData?api-version=7.2-preview.1`,
160+
{
161+
method: 'GET',
162+
headers: defaultHeaders,
163+
},
164+
);
165+
166+
if (!connectionDataResponse.ok) {
167+
const errorText = await connectionDataResponse.text();
168+
throw new Error(
169+
`Failed to fetch connection data: HTTP ${String(connectionDataResponse.status)}: ${errorText}`,
170+
);
171+
}
172+
173+
const connectionData = (await connectionDataResponse.json()) as LocationData;
174+
175+
const authenticatedUser = connectionData.authenticatedUser;
176+
177+
if (!authenticatedUser?.id) {
178+
throw new Error('Could not determine authenticated user');
179+
}
180+
181+
console.log(
182+
`Authenticated as: ${authenticatedUser.customDisplayName ?? authenticatedUser.providerDisplayName} (ID: ${authenticatedUser.id})`,
183+
);
184+
185+
return authenticatedUser;
186+
}
187+
188+
/**
189+
* Set the merge commit message for a pull request using squash merge strategy
190+
*/
191+
async function setMergeCommitMessageAndAutocomplete({
192+
pullRequestId,
193+
mergeCommitMessage,
194+
token,
195+
org,
196+
project,
197+
repo,
198+
}: {
199+
pullRequestId: string;
200+
mergeCommitMessage: string;
201+
org: string;
202+
project: string;
203+
repo: string;
204+
token: string;
205+
}) {
206+
const defaultHeaders = {
207+
Authorization: `Basic ${Buffer.from(`:${token}`).toString('base64')}`,
208+
'Content-Type': 'application/json',
209+
};
210+
211+
const authenticatedUser = await getAuthenticatedUser(org, defaultHeaders);
212+
213+
console.log(
214+
`Setting autocomplete and merge commit message for PR #${pullRequestId} as ${authenticatedUser.customDisplayName ?? authenticatedUser.providerDisplayName} (${authenticatedUser.id})...`,
215+
);
216+
217+
const updateData = {
218+
autoCompleteSetBy: {
219+
id: authenticatedUser.id,
220+
},
221+
completionOptions: {
222+
mergeStrategy: 'squash',
223+
mergeCommitMessage,
224+
},
225+
};
226+
227+
const response = await fetch(
228+
`https://dev.azure.com/${org}/${project}/_apis/git/repositories/${repo}/pullrequests/${pullRequestId}?api-version=7.1`,
229+
{
230+
method: 'PATCH',
231+
headers: defaultHeaders,
232+
body: JSON.stringify(updateData),
233+
},
234+
);
235+
236+
if (!response.ok) {
237+
const errorText = await response.text();
238+
throw new Error(
239+
`Failed to set autocomplete and merge commit message: HTTP ${String(response.status)}: ${errorText}`,
240+
);
241+
}
242+
243+
console.log(
244+
`Successfully set autocomplete and merge commit message for PR #${pullRequestId}`,
245+
);
246+
}
247+
248+
async function main() {
249+
const { token, currentPullRequestId, org, project, repo } = getArgs();
250+
251+
await setMergeCommitMessageAndAutocomplete({
252+
pullRequestId: currentPullRequestId,
253+
mergeCommitMessage: 'feat: conventional commit message', // <- set your commit message here
254+
token,
255+
org,
256+
project,
257+
repo,
258+
});
259+
}
260+
261+
main().catch((err: unknown) => {
262+
const errorMessage =
263+
err instanceof Error ? err.message : 'Unknown error occurred';
264+
console.error(`[ERROR] ${errorMessage}`);
265+
throw err;
266+
});
267+
```
268+
269+
This can also be run locally with `node ./set-autocomplete-and-commit-message.ts --token [PAT TOKEN WITH SCOPES: vso.code_write and vso.identity] --pr-id [PULL_REQUEST_ID] --org [NAME_OF_ORGANISATION] --project [NAME_OF_PROJECT] --repo [NAME_OF_REPOSITORY]`. You'll need Node.js 24 or later to run this. (And yes, you can run TypeScript files directly with Node.js these days).
270+
271+
The thing I haven't included here is how you determine the `mergeCommitMessage`. In my case, I use the title of the pull request as the commit message. You can fetch the pull request details using the Azure DevOps API and extract the title. I left this out for brevity, but you can easily add it in. Or use whatever logic you like to determine the commit message. The point is that you have control over it.
272+
273+
### The build validation pipeline
274+
275+
Now we have our script, we need to run it in a build validation pipeline. Here's an example of an Azure DevOps pipeline that runs the script:
276+
277+
```yml
278+
trigger: none
279+
280+
pool:
281+
vmImage: ubuntu-latest
282+
283+
variables:
284+
isPullRequest: ${{ eq(variables['Build.Reason'], 'PullRequest') }}
285+
286+
stages:
287+
- stage: AutoCompleteAndCommitMessage
288+
displayName: Set autocomplete and commit message
289+
condition: ${{ variables['isPullRequest'] }}
290+
jobs:
291+
- job:
292+
steps:
293+
- task: NodeTool@0
294+
inputs:
295+
versionSpec: 24
296+
displayName: Install Node.js
297+
298+
- bash: node ./scripts/set-autocomplete-and-commit-message.ts --token $(System.AccessToken) --pr-id $(System.PullRequest.PullRequestId) --org "$(System.CollectionUri)" --project "$(System.TeamProject)" --repo "$(Build.Repository.Name)"
299+
displayName: Set autocomplete and commit message
300+
```
301+
302+
Crucially, this pipeline is triggered only for pull requests; the `System.PullRequest.PullRequestId` is only available in build validations run as part of a pull request. The pipeline installs Node.js 24 and then runs our script, passing in the necessary parameters. The `System.AccessToken` is used to authenticate with the Azure DevOps API, so make sure that the pipeline has the necessary permissions to use it.
303+
304+
## Conclusion
305+
306+
And that's it! With this setup, you can maintain a git commit history of conventional commits in Azure DevOps, even when merging pull requests. By using the Azure DevOps API to set the merge commit message when setting a pull request to autocomplete, you can ensure that your commit messages are meaningful and consistent.
6.93 KB
Loading
5.69 KB
Loading
12.5 KB
Loading

0 commit comments

Comments
 (0)