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

This commit is contained in:
joegoldman2 2023-12-27 11:17:03 +02:00 committed by GitHub
parent 84270beec4
commit 1786438d33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 210 additions and 10 deletions

View file

@ -1828,7 +1828,7 @@ const options: RenovateOptions[] = [
type: 'string', type: 'string',
allowedValues: ['auto', 'fast-forward', 'merge-commit', 'rebase', 'squash'], allowedValues: ['auto', 'fast-forward', 'merge-commit', 'rebase', 'squash'],
default: 'auto', default: 'auto',
supportedPlatforms: ['bitbucket', 'gitea'], supportedPlatforms: ['azure', 'bitbucket', 'gitea'],
}, },
{ {
name: 'automergeComment', name: 'automergeComment',

View file

@ -2,6 +2,7 @@ import { Readable } from 'node:stream';
import is from '@sindresorhus/is'; import is from '@sindresorhus/is';
import type { IGitApi } from 'azure-devops-node-api/GitApi'; import type { IGitApi } from 'azure-devops-node-api/GitApi';
import { import {
GitPullRequest,
GitPullRequestMergeStrategy, GitPullRequestMergeStrategy,
GitStatusState, GitStatusState,
PullRequestStatus, PullRequestStatus,
@ -936,7 +937,7 @@ describe('modules/platform/azure/index', () => {
expect(pr).toMatchSnapshot(); 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' }); await initRepo({ repository: 'some/repo' });
const prResult = [ const prResult = [
{ {
@ -1001,7 +1002,10 @@ describe('modules/platform/azure/index', () => {
prTitle: 'The Title', prTitle: 'The Title',
prBody: 'Hello world', prBody: 'Hello world',
labels: ['deps', 'renovate'], labels: ['deps', 'renovate'],
platformOptions: { usePlatformAutomerge: true }, platformOptions: {
automergeStrategy: 'auto',
usePlatformAutomerge: true,
},
}); });
await azure.createPr({ await azure.createPr({
@ -1010,12 +1014,128 @@ describe('modules/platform/azure/index', () => {
prTitle: 'The Second Title', prTitle: 'The Second Title',
prBody: 'Hello world', prBody: 'Hello world',
labels: ['deps', 'renovate'], labels: ['deps', 'renovate'],
platformOptions: { usePlatformAutomerge: true }, platformOptions: {
automergeStrategy: 'auto',
usePlatformAutomerge: true,
},
}); });
expect(updateFn).toHaveBeenCalledTimes(2); expect(updateFn).toHaveBeenCalledTimes(2);
expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(1); 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 () => { it('should create and return an approved PR object', async () => {
@ -1528,6 +1648,7 @@ describe('modules/platform/azure/index', () => {
const res = await azure.mergePr({ const res = await azure.mergePr({
branchName: branchNameMock, branchName: branchNameMock,
id: pullRequestIdMock, id: pullRequestIdMock,
strategy: 'auto',
}); });
expect(updatePullRequestMock).toHaveBeenCalledWith( expect(updatePullRequestMock).toHaveBeenCalledWith(
@ -1546,6 +1667,59 @@ describe('modules/platform/azure/index', () => {
expect(res).toBeTrue(); 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 () => { it('should return false if the PR does not update successfully', async () => {
await initRepo({ repository: 'some/repo' }); await initRepo({ repository: 'some/repo' });
const pullRequestIdMock = 12345; const pullRequestIdMock = 12345;
@ -1593,10 +1767,12 @@ describe('modules/platform/azure/index', () => {
await azure.mergePr({ await azure.mergePr({
branchName: 'test-branch-1', branchName: 'test-branch-1',
id: 1234, id: 1234,
strategy: 'auto',
}); });
await azure.mergePr({ await azure.mergePr({
branchName: 'test-branch-2', branchName: 'test-branch-2',
id: 5678, id: 5678,
strategy: 'auto',
}); });
expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(1); expect(azureHelper.getMergeMethod).toHaveBeenCalledTimes(1);

View file

@ -52,6 +52,7 @@ import {
getRenovatePRFormat, getRenovatePRFormat,
getRepoByName, getRepoByName,
getStorageExtraCloneOpts, getStorageExtraCloneOpts,
mapMergeStrategy,
max4000Chars, max4000Chars,
} from './util'; } from './util';
@ -491,7 +492,10 @@ export async function createPr({
config.repoId, config.repoId,
); );
if (platformOptions?.usePlatformAutomerge) { 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( pr = await azureApiGit.updatePullRequest(
{ {
autoCompleteSetBy: { autoCompleteSetBy: {
@ -736,13 +740,17 @@ export async function setBranchStatus({
export async function mergePr({ export async function mergePr({
branchName, branchName,
id: pullRequestId, id: pullRequestId,
strategy,
}: MergePRConfig): Promise<boolean> { }: MergePRConfig): Promise<boolean> {
logger.debug(`mergePr(${pullRequestId}, ${branchName!})`); logger.debug(`mergePr(${pullRequestId}, ${branchName!})`);
const azureApiGit = await azureApi.gitApi(); const azureApiGit = await azureApi.gitApi();
let pr = await azureApiGit.getPullRequestById(pullRequestId, config.project); 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 = { const objToUpdate: GitPullRequest = {
status: PullRequestStatus.Completed, status: PullRequestStatus.Completed,
lastMergeSourceCommit: pr.lastMergeSourceCommit, lastMergeSourceCommit: pr.lastMergeSourceCommit,

View file

@ -18,10 +18,6 @@ Permissions for your PAT should be at minimum:
Remember to set `platform=azure` somewhere in your Renovate config file. 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 ## Running Renovate in Azure Pipelines
### Setting up a new pipeline ### Setting up a new pipeline

View file

@ -1,9 +1,11 @@
import { import {
GitPullRequest, GitPullRequest,
GitPullRequestMergeStrategy,
GitRepository, GitRepository,
GitStatusContext, GitStatusContext,
PullRequestStatus, PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces.js'; } from 'azure-devops-node-api/interfaces/GitInterfaces.js';
import type { MergeStrategy } from '../../../config/types';
import { logger } from '../../../logger'; import { logger } from '../../../logger';
import type { HostRule, PrState } from '../../../types'; import type { HostRule, PrState } from '../../../types';
import type { GitOptions } from '../../../types/git'; import type { GitOptions } from '../../../types/git';
@ -181,3 +183,19 @@ export function getRepoByName(
} }
return foundRepo ?? null; 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;
}
}

View file

@ -96,6 +96,7 @@ export interface Issue {
} }
export type PlatformPrOptions = { export type PlatformPrOptions = {
autoApprove?: boolean; autoApprove?: boolean;
automergeStrategy?: MergeStrategy;
azureWorkItemId?: number; azureWorkItemId?: number;
bbUseDefaultReviewers?: boolean; bbUseDefaultReviewers?: boolean;
gitLabIgnoreApprovals?: boolean; gitLabIgnoreApprovals?: boolean;

View file

@ -55,6 +55,7 @@ export function getPlatformPrOptions(
return { return {
autoApprove: !!config.autoApprove, autoApprove: !!config.autoApprove,
automergeStrategy: config.automergeStrategy,
azureWorkItemId: config.azureWorkItemId ?? 0, azureWorkItemId: config.azureWorkItemId ?? 0,
bbUseDefaultReviewers: !!config.bbUseDefaultReviewers, bbUseDefaultReviewers: !!config.bbUseDefaultReviewers,
gitLabIgnoreApprovals: !!config.gitLabIgnoreApprovals, gitLabIgnoreApprovals: !!config.gitLabIgnoreApprovals,