Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(platform/azure): implement automergeStrategy for Azure DevOps platform #26429

Merged
merged 10 commits into from
Dec 27, 2023
2 changes: 1 addition & 1 deletion lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1828,7 +1828,7 @@ const options: RenovateOptions[] = [
type: 'string',
allowedValues: ['auto', 'fast-forward', 'merge-commit', 'rebase', 'squash'],
default: 'auto',
supportedPlatforms: ['bitbucket', 'gitea'],
supportedPlatforms: ['azure', 'bitbucket', 'gitea'],
},
{
name: 'automergeComment',
Expand Down
182 changes: 179 additions & 3 deletions lib/modules/platform/azure/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Readable } from 'node:stream';
import is from '@sindresorhus/is';
import type { IGitApi } from 'azure-devops-node-api/GitApi';
import {
GitPullRequest,
GitPullRequestMergeStrategy,
GitStatusState,
PullRequestStatus,
Expand Down Expand Up @@ -936,7 +937,7 @@ describe('modules/platform/azure/index', () => {
expect(pr).toMatchSnapshot();
});

it('should only call getMergeMethod once per run', async () => {
it('should only call getMergeMethod once per run when automergeStrategy is auto', async () => {
await initRepo({ repository: 'some/repo' });
const prResult = [
{
Expand Down Expand Up @@ -1001,7 +1002,10 @@ describe('modules/platform/azure/index', () => {
prTitle: 'The Title',
prBody: 'Hello world',
labels: ['deps', 'renovate'],
platformOptions: { usePlatformAutomerge: true },
platformOptions: {
automergeStrategy: 'auto',
usePlatformAutomerge: true,
},
});

await azure.createPr({
Expand All @@ -1010,12 +1014,128 @@ describe('modules/platform/azure/index', () => {
prTitle: 'The Second Title',
prBody: 'Hello world',
labels: ['deps', 'renovate'],
platformOptions: { usePlatformAutomerge: true },
platformOptions: {
automergeStrategy: 'auto',
usePlatformAutomerge: true,
},
});

expect(updateFn).toHaveBeenCalledTimes(2);
expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(1);
});

it.each`
automergeStrategy
${'fast-forward'}
${'merge-commit'}
${'rebase'}
${'squash'}
`(
'should not call getMergeMethod when automergeStrategy is $automergeStrategy',
async (automergeStrategy) => {
await initRepo({ repository: 'some/repo' });
const prResult = {
pullRequestId: 123,
title: 'The Title',
createdBy: {
id: '123',
},
};
const prUpdateResults = {
...prResult,
autoCompleteSetBy: {
id: prResult.createdBy.id,
},
completionOptions: {
squashMerge: true,
deleteSourceBranch: true,
mergeCommitMessage: 'The Title',
},
};
const updateFn = jest.fn(() => Promise.resolve(prUpdateResults));

azureApi.gitApi.mockResolvedValue(
partial<IGitApi>({
createPullRequest: jest.fn(() => Promise.resolve(prResult)),
createPullRequestLabel: jest.fn().mockResolvedValue({}),
updatePullRequest: updateFn,
}),
);
await azure.createPr({
sourceBranch: 'some-branch',
targetBranch: 'dev',
prTitle: 'The Title',
prBody: 'Hello world',
labels: ['deps', 'renovate'],
platformOptions: {
automergeStrategy,
usePlatformAutomerge: true,
},
});

expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(0);
},
);

it.each`
automergeStrategy | prMergeStrategy
${'fast-forward'} | ${GitPullRequestMergeStrategy.Rebase}
${'merge-commit'} | ${GitPullRequestMergeStrategy.NoFastForward}
${'rebase'} | ${GitPullRequestMergeStrategy.Rebase}
${'squash'} | ${GitPullRequestMergeStrategy.Squash}
`(
'should create PR with mergeStrategy $prMergeStrategy',
async ({ automergeStrategy, prMergeStrategy }) => {
await initRepo({ repository: 'some/repo' });
const prResult = {
pullRequestId: 456,
title: 'The Title',
createdBy: {
id: '123',
},
};
const prUpdateResult = {
...prResult,
autoCompleteSetBy: {
id: prResult.createdBy.id,
},
completionOptions: {
mergeStrategy: prMergeStrategy,
squashMerge: false,
deleteSourceBranch: true,
mergeCommitMessage: 'The Title',
},
};
const updateFn = jest.fn().mockResolvedValue(prUpdateResult);
azureApi.gitApi.mockResolvedValueOnce(
partial<IGitApi>({
createPullRequest: jest.fn().mockResolvedValue(prResult),
createPullRequestLabel: jest.fn().mockResolvedValue({}),
updatePullRequest: updateFn,
}),
);
const pr = await azure.createPr({
sourceBranch: 'some-branch',
targetBranch: 'dev',
prTitle: 'The Title',
prBody: 'Hello world',
labels: ['deps', 'renovate'],
platformOptions: {
automergeStrategy,
usePlatformAutomerge: true,
},
});

expect((pr as GitPullRequest).completionOptions?.mergeStrategy).toBe(
prMergeStrategy,
);
expect(updateFn).toHaveBeenCalled();
expect(
updateFn.mock.calls[0][0].completionOptions.mergeStrategy,
).toBe(prMergeStrategy);
expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(0);
},
);
});

it('should create and return an approved PR object', async () => {
Expand Down Expand Up @@ -1528,6 +1648,7 @@ describe('modules/platform/azure/index', () => {
const res = await azure.mergePr({
branchName: branchNameMock,
id: pullRequestIdMock,
strategy: 'auto',
});

expect(updatePullRequestMock).toHaveBeenCalledWith(
Expand All @@ -1546,6 +1667,59 @@ describe('modules/platform/azure/index', () => {
expect(res).toBeTrue();
});

it.each`
automergeStrategy | prMergeStrategy
${'fast-forward'} | ${GitPullRequestMergeStrategy.Rebase}
${'merge-commit'} | ${GitPullRequestMergeStrategy.NoFastForward}
${'rebase'} | ${GitPullRequestMergeStrategy.Rebase}
${'squash'} | ${GitPullRequestMergeStrategy.Squash}
`(
'should complete PR with mergeStrategy $prMergeStrategy',
async ({ automergeStrategy, prMergeStrategy }) => {
await initRepo({ repository: 'some/repo' });
const pullRequestIdMock = 12345;
const branchNameMock = 'test';
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
const updatePullRequestMock = jest.fn(() => ({
status: 3,
}));
azureApi.gitApi.mockImplementationOnce(
() =>
({
getPullRequestById: jest.fn(() => ({
lastMergeSourceCommit: lastMergeSourceCommitMock,
targetRefName: 'refs/heads/ding',
title: 'title',
})),
updatePullRequest: updatePullRequestMock,
}) as any,
);

azureHelper.getMergeMethod = jest.fn().mockReturnValue(prMergeStrategy);

const res = await azure.mergePr({
branchName: branchNameMock,
id: pullRequestIdMock,
strategy: automergeStrategy,
});

expect(updatePullRequestMock).toHaveBeenCalledWith(
{
status: PullRequestStatus.Completed,
lastMergeSourceCommit: lastMergeSourceCommitMock,
completionOptions: {
mergeStrategy: prMergeStrategy,
deleteSourceBranch: true,
mergeCommitMessage: 'title',
},
},
'1',
pullRequestIdMock,
);
expect(res).toBeTrue();
},
);

it('should return false if the PR does not update successfully', async () => {
await initRepo({ repository: 'some/repo' });
const pullRequestIdMock = 12345;
Expand Down Expand Up @@ -1593,10 +1767,12 @@ describe('modules/platform/azure/index', () => {
await azure.mergePr({
branchName: 'test-branch-1',
id: 1234,
strategy: 'auto',
});
await azure.mergePr({
branchName: 'test-branch-2',
id: 5678,
strategy: 'auto',
});

expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(1);
Expand Down
12 changes: 10 additions & 2 deletions lib/modules/platform/azure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
getRenovatePRFormat,
getRepoByName,
getStorageExtraCloneOpts,
mapMergeStrategy,
max4000Chars,
} from './util';

Expand Down Expand Up @@ -491,7 +492,10 @@ export async function createPr({
config.repoId,
);
if (platformOptions?.usePlatformAutomerge) {
const mergeStrategy = await getMergeStrategy(pr.targetRefName!);
const mergeStrategy =
platformOptions.automergeStrategy === 'auto'
? await getMergeStrategy(pr.targetRefName!)
: mapMergeStrategy(platformOptions.automergeStrategy);
pr = await azureApiGit.updatePullRequest(
{
autoCompleteSetBy: {
Expand Down Expand Up @@ -736,13 +740,17 @@ export async function setBranchStatus({
export async function mergePr({
branchName,
id: pullRequestId,
strategy,
}: MergePRConfig): Promise<boolean> {
logger.debug(`mergePr(${pullRequestId}, ${branchName!})`);
const azureApiGit = await azureApi.gitApi();

let pr = await azureApiGit.getPullRequestById(pullRequestId, config.project);

const mergeStrategy = await getMergeStrategy(pr.targetRefName!);
const mergeStrategy =
strategy === 'auto'
? await getMergeStrategy(pr.targetRefName!)
: mapMergeStrategy(strategy);
const objToUpdate: GitPullRequest = {
status: PullRequestStatus.Completed,
lastMergeSourceCommit: pr.lastMergeSourceCommit,
Expand Down
4 changes: 0 additions & 4 deletions lib/modules/platform/azure/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ Permissions for your PAT should be at minimum:

Remember to set `platform=azure` somewhere in your Renovate config file.

## Features awaiting implementation

- The `automergeStrategy` configuration option has not been implemented for this platform, and all values behave as if the value `auto` was used. Renovate will use the merge strategy configured in the Azure Repos repository itself, and this cannot be overridden yet

## Running Renovate in Azure Pipelines

### Setting up a new pipeline
Expand Down
18 changes: 18 additions & 0 deletions lib/modules/platform/azure/util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
GitPullRequest,
GitPullRequestMergeStrategy,
GitRepository,
GitStatusContext,
PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces.js';
import type { MergeStrategy } from '../../../config/types';
import { logger } from '../../../logger';
import type { HostRule, PrState } from '../../../types';
import type { GitOptions } from '../../../types/git';
Expand Down Expand Up @@ -181,3 +183,19 @@ export function getRepoByName(
}
return foundRepo ?? null;
}

export function mapMergeStrategy(
mergeStrategy?: MergeStrategy,
): GitPullRequestMergeStrategy {
switch (mergeStrategy) {
case 'rebase':
case 'fast-forward':
return GitPullRequestMergeStrategy.Rebase;
case 'merge-commit':
return GitPullRequestMergeStrategy.NoFastForward;
case 'squash':
return GitPullRequestMergeStrategy.Squash;
default:
return GitPullRequestMergeStrategy.NoFastForward;
}
}
1 change: 1 addition & 0 deletions lib/modules/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export interface Issue {
}
export type PlatformPrOptions = {
autoApprove?: boolean;
automergeStrategy?: MergeStrategy;
azureWorkItemId?: number;
bbUseDefaultReviewers?: boolean;
gitLabIgnoreApprovals?: boolean;
Expand Down
1 change: 1 addition & 0 deletions lib/workers/repository/update/pr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function getPlatformPrOptions(

return {
autoApprove: !!config.autoApprove,
automergeStrategy: config.automergeStrategy,
azureWorkItemId: config.azureWorkItemId ?? 0,
bbUseDefaultReviewers: !!config.bbUseDefaultReviewers,
gitLabIgnoreApprovals: !!config.gitLabIgnoreApprovals,
Expand Down