diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 749288a56a2b5a43e2fdb21e4ac4723fa9de7200..c03a0ab9219b8c9b94947af47d339148cd6531a8 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -193,6 +193,12 @@ If you prefer that Renovate more silently automerge _without_ Pull Requests at a The final value for `automergeType` is `"pr-comment"`, intended only for users who already have a "merge bot" such as [bors-ng](https://github.com/bors-ng/bors-ng) and want Renovate to _not_ actually automerge by itself and instead tell `bors-ng` to merge for it, by using a comment in the PR. If you're not already using `bors-ng` or similar, don't worry about this option. +## azureAutoApprove + +Setting this to `true` will automatically approve the PRs in Azure DevOps. + +You can also configure this using `packageRules` if you want to use it selectively (e.g. per-package). + ## azureAutoComplete Setting this to `true` will configure PRs in Azure DevOps to auto-complete after all (if any) branch policies have been met. diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts index f03a692e3b37b4223320f73995f69b094fb87a2c..1b2fc1a4e50009e170b3aff3ab68b33d3c471d15 100644 --- a/lib/config/definitions.ts +++ b/lib/config/definitions.ts @@ -716,6 +716,13 @@ const options: RenovateOptions[] = [ type: 'integer', default: 0, }, + { + name: 'azureAutoApprove', + description: + 'If set to true, Azure DevOps PRs will be automatically approved.', + type: 'boolean', + default: false, + }, // depType { name: 'ignoreDeps', diff --git a/lib/platform/azure/__snapshots__/index.spec.ts.snap b/lib/platform/azure/__snapshots__/index.spec.ts.snap index 8616755105d0be9855cb2618024457a53e2ca174..cf109e9d5e2146327c8a7b97b3422833ebc8bdea 100644 --- a/lib/platform/azure/__snapshots__/index.spec.ts.snap +++ b/lib/platform/azure/__snapshots__/index.spec.ts.snap @@ -52,6 +52,32 @@ Object { } `; +exports[`platform/azure/index createPr() should create and return an approved PR object 1`] = ` +Object { + "body": undefined, + "createdAt": undefined, + "createdBy": Object { + "id": 123, + "url": "user-url", + }, + "displayNumber": "Pull Request #456", + "number": 456, + "pullRequestId": 456, + "reviewers": Array [ + Object { + "isFlagged": false, + "isRequired": false, + "reviewerUrl": "user-url", + "vote": 10, + }, + ], + "sourceBranch": undefined, + "sourceRefName": undefined, + "state": "open", + "targetBranch": undefined, +} +`; + exports[`platform/azure/index deleteLabel() Should delete a label 1`] = ` Array [ Array [], diff --git a/lib/platform/azure/index.spec.ts b/lib/platform/azure/index.spec.ts index 4efe9a59e32ab9742945e927e887379ba12b7fb8..a03dcb6c5b75dd85727c33ee2e5f9be8c5ceddd4 100644 --- a/lib/platform/azure/index.spec.ts +++ b/lib/platform/azure/index.spec.ts @@ -11,6 +11,7 @@ import { BranchStatus, PrState } from '../../types'; import * as _git from '../../util/git'; import * as _hostRules from '../../util/host-rules'; import type { Platform, RepoParams } from '../types'; +import { AzurePrVote } from './types'; describe(getName(), () => { let hostRules: jest.Mocked<typeof _hostRules>; @@ -665,6 +666,49 @@ describe(getName(), () => { expect(updateFn).toHaveBeenCalled(); expect(pr).toMatchSnapshot(); }); + it('should create and return an approved PR object', async () => { + await initRepo({ repository: 'some/repo' }); + const prResult = { + pullRequestId: 456, + displayNumber: 'Pull Request #456', + createdBy: { + id: 123, + url: 'user-url', + }, + }; + const prUpdateResult = { + ...prResult, + reviewers: [ + { + reviewerUrl: prResult.createdBy.url, + vote: AzurePrVote.Approved, + isFlagged: false, + isRequired: false, + }, + ], + }; + const updateFn = jest + .fn(() => prUpdateResult) + .mockName('createPullRequestReviewer'); + azureApi.gitApi.mockImplementationOnce( + () => + ({ + createPullRequest: jest.fn(() => prResult), + createPullRequestLabel: jest.fn(() => ({})), + createPullRequestReviewer: updateFn, + } as any) + ); + const pr = await azure.createPr({ + sourceBranch: 'some-branch', + targetBranch: 'dev', + prTitle: 'The Title', + prBody: 'Hello world', + labels: ['deps', 'renovate'], + platformOptions: { azureAutoApprove: true }, + }); + expect(updateFn).toHaveBeenCalled(); + expect(pr).toMatchSnapshot(); + }); }); describe('updatePr(prNo, title, body)', () => { diff --git a/lib/platform/azure/index.ts b/lib/platform/azure/index.ts index bd5f96e417d0c7179f2c82f169dd829f167e1d69..a996eb317643a38a3209a065ad378dca264efb69 100644 --- a/lib/platform/azure/index.ts +++ b/lib/platform/azure/index.ts @@ -34,7 +34,7 @@ import type { import { smartTruncate } from '../utils/pr-body'; import * as azureApi from './azure-got-wrapper'; import * as azureHelper from './azure-helper'; -import type { AzurePr } from './types'; +import { AzurePr, AzurePrVote } from './types'; import { getBranchNameWithoutRefsheadsPrefix, getGitStatusContextCombinedName, @@ -416,6 +416,19 @@ export async function createPr({ pr.pullRequestId ); } + if (platformOptions?.azureAutoApprove) { + pr = await azureApiGit.createPullRequestReviewer( + { + reviewerUrl: pr.createdBy.url, + vote: AzurePrVote.Approved, + isFlagged: false, + isRequired: false, + }, + config.repoId, + pr.pullRequestId, + pr.createdBy.id + ); + } await Promise.all( labels.map((label) => azureApiGit.createPullRequestLabel( diff --git a/lib/platform/azure/types.ts b/lib/platform/azure/types.ts index 1ee6e300594196fdb7025561d66322b80e3bb896..910f15d4933f0557c33d2b1fc355c39e9b5b37fc 100644 --- a/lib/platform/azure/types.ts +++ b/lib/platform/azure/types.ts @@ -3,3 +3,11 @@ import { Pr } from '../types'; export interface AzurePr extends Pr { sourceRefName?: string; } + +export enum AzurePrVote { + NoVote = 0, + Reject = -10, + WaitingForAuthor = -5, + ApprovedWithSuggestions = 5, + Approved = 10, +} diff --git a/lib/platform/types.ts b/lib/platform/types.ts index 467dafdd67cf1a7ae4c889997be7515ff09c0e23..d52a7e6a909c0bfb9ac96b7c577e71bfe5e7b74a 100644 --- a/lib/platform/types.ts +++ b/lib/platform/types.ts @@ -73,6 +73,7 @@ export interface Issue { title?: string; } export type PlatformPrOptions = { + azureAutoApprove?: boolean; azureAutoComplete?: boolean; azureWorkItemId?: number; bbUseDefaultReviewers?: boolean; diff --git a/lib/workers/pr/__snapshots__/index.spec.ts.snap b/lib/workers/pr/__snapshots__/index.spec.ts.snap index 4216b98d8917568874737e723be10a2906553306..7e4db45ae475a66d1d14c1093e92f8cc541cf788 100644 --- a/lib/workers/pr/__snapshots__/index.spec.ts.snap +++ b/lib/workers/pr/__snapshots__/index.spec.ts.snap @@ -58,6 +58,7 @@ Array [ "draftPR": false, "labels": Array [], "platformOptions": Object { + "azureAutoApprove": false, "azureAutoComplete": false, "azureWorkItemId": 0, "bbUseDefaultReviewers": true, @@ -90,6 +91,7 @@ Array [ "draftPR": false, "labels": Array [], "platformOptions": Object { + "azureAutoApprove": false, "azureAutoComplete": false, "azureWorkItemId": 0, "bbUseDefaultReviewers": true, @@ -109,6 +111,7 @@ Array [ "draftPR": false, "labels": Array [], "platformOptions": Object { + "azureAutoApprove": false, "azureAutoComplete": false, "azureWorkItemId": 0, "bbUseDefaultReviewers": true, @@ -128,6 +131,7 @@ Array [ "draftPR": false, "labels": Array [], "platformOptions": Object { + "azureAutoApprove": false, "azureAutoComplete": false, "azureWorkItemId": 0, "bbUseDefaultReviewers": true, @@ -147,6 +151,7 @@ Array [ "draftPR": false, "labels": Array [], "platformOptions": Object { + "azureAutoApprove": false, "azureAutoComplete": false, "azureWorkItemId": 0, "bbUseDefaultReviewers": true, diff --git a/lib/workers/pr/index.ts b/lib/workers/pr/index.ts index 92efafb868ae10da446b31c53398f38c0871d547..8fe83d2f7a00ca0a040c4695e182bf4e96b34e0f 100644 --- a/lib/workers/pr/index.ts +++ b/lib/workers/pr/index.ts @@ -109,6 +109,7 @@ export function getPlatformPrOptions( config: RenovateConfig & PlatformPrOptions ): PlatformPrOptions { return { + azureAutoApprove: config.azureAutoApprove, azureAutoComplete: config.azureAutoComplete, azureWorkItemId: config.azureWorkItemId, bbUseDefaultReviewers: config.bbUseDefaultReviewers,