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): Allow to bypass certain policies for auto-merge #33626

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,27 @@ 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.

## azureBypassPolicyMinimumNumberOfReviewers

If you have branch policies in Azure DevOps that require a minimum number of reviewers, you can set this to `true` to bypass this policy.

## azureBypassPolicyReason

If renovate uses bypass to automerge this will be the reason for the bypass.

## azureBypassPolicyRequiredReviewers

If you have branch policies in Azure DevOps that set required reviewers, you can set this to `true` to bypass this policy.

## azureBypassPolicyTypeUuids

If you have branch policies in Azure DevOps that prevent Renovate from merging PRs, you can set this to a list of UUIDs of the policies you want to bypass.

## azureBypassPolicyWorkItemLinking

If you have branch policies in Azure DevOps that require work item linking, you can set this to `true` to bypass this policy
This can be used instead of setting `azureWorkItemId`.

## azureWorkItemId

When creating a PR in Azure DevOps, some branches can be protected with branch policies to [check for linked work items](https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops#check-for-linked-work-items).
Expand Down
20 changes: 20 additions & 0 deletions lib/config/__snapshots__/validation.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ exports[`config/validation validateConfig(config) catches invalid templates 1`]

exports[`config/validation validateConfig(config) errors for all types 1`] = `
[
{
"message": "Configuration option \`azureBypassPolicyMinimumNumberOfReviewers\` should be boolean. Found: 1 (number)",
"topic": "Configuration Error",
},
{
"message": "Configuration option \`azureBypassPolicyReason\` should be boolean. Found: 1 (number)",
"topic": "Configuration Error",
},
{
"message": "Configuration option \`azureBypassPolicyRequiredReviewers\` should be boolean. Found: 1 (number)",
"topic": "Configuration Error",
},
{
"message": "Configuration option \`azureBypassPolicyTypeUuids\` should be a list (Array)",
"topic": "Configuration Error",
},
{
"message": "Configuration option \`azureBypassPolicyWorkItemLinking\` should be boolean. Found: 1 (number)",
"topic": "Configuration Error",
},
{
"message": "Configuration option \`azureWorkItemId\` should be an integer. Found: false (boolean).",
"topic": "Configuration Error",
Expand Down
36 changes: 36 additions & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,42 @@ const options: RenovateOptions[] = [
default: 0,
supportedPlatforms: ['azure'],
},
{
name: 'azureBypassPolicyRequiredReviewers',
description: 'Allow to bypass the policy for required reviewers.',
type: 'boolean',
default: false,
supportedPlatforms: ['azure'],
},
{
name: 'azureBypassPolicyMinimumNumberOfReviewers',
description: 'Allow to bypass the policy for minimum number of reviewers.',
type: 'boolean',
default: false,
supportedPlatforms: ['azure'],
},
{
name: 'azureBypassPolicyWorkItemLinking',
description: 'Allow to bypass the policy for work item linking.',
type: 'boolean',
default: false,
supportedPlatforms: ['azure'],
},
{
name: 'azureBypassPolicyTypeUuids',
description: 'A list of policy type UUIDs to allow bypassing.',
type: 'array',
subType: 'string',
default: [],
supportedPlatforms: ['azure'],
},
{
name: 'azureBypassPolicyReason',
description: 'The reason to set when bypassing policies.',
type: 'boolean',
default: false,
supportedPlatforms: ['azure'],
},
{
name: 'autoApprove',
description: 'Set to `true` to automatically approve PRs.',
Expand Down
7 changes: 6 additions & 1 deletion lib/config/validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,11 @@ describe('config/validation', () => {
timezone: 'Asia',
labels: 5 as any,
azureWorkItemId: false as any,
azureBypassPolicyRequiredReviewers: 1 as any,
azureBypassPolicyMinimumNumberOfReviewers: 1 as any,
azureBypassPolicyWorkItemLinking: 1 as any,
azureBypassPolicyTypeUuids: 2 as any,
azureBypassPolicyReason: 1 as any,
semanticCommitType: 7 as any,
lockFileMaintenance: false as any,
extends: [':timezone(Europe/Brussel)'],
Expand All @@ -519,7 +524,7 @@ describe('config/validation', () => {
);
expect(warnings).toHaveLength(1);
expect(errors).toMatchSnapshot();
expect(errors).toHaveLength(12);
expect(errors).toHaveLength(17);
});

it('selectors outside packageRules array trigger errors', async () => {
Expand Down
24 changes: 12 additions & 12 deletions lib/modules/datasource/artifactory/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ exports[`modules/datasource/artifactory/index getReleases parses real data (file
"registryUrl": "https://jfrog.company.com/artifactory",
"releases": [
{
"releaseTimestamp": "2021-07-21T20:08:00.000Z",
"releaseTimestamp": "2021-07-21T21:08:00.000Z",
"version": "1.0.0",
},
{
"releaseTimestamp": "2021-08-23T20:03:00.000Z",
"releaseTimestamp": "2021-08-23T21:03:00.000Z",
"version": "1.0.1",
},
{
"releaseTimestamp": "2021-07-21T20:09:00.000Z",
"releaseTimestamp": "2021-07-21T21:09:00.000Z",
"version": "1.0.2",
},
{
"releaseTimestamp": "2021-02-06T09:54:00.000Z",
"releaseTimestamp": "2021-02-06T10:54:00.000Z",
"version": "1.0.3",
},
],
Expand All @@ -29,19 +29,19 @@ exports[`modules/datasource/artifactory/index getReleases parses real data (fold
"registryUrl": "https://jfrog.company.com/artifactory",
"releases": [
{
"releaseTimestamp": "2021-07-21T20:08:00.000Z",
"releaseTimestamp": "2021-07-21T21:08:00.000Z",
"version": "1.0.0",
},
{
"releaseTimestamp": "2021-08-23T20:03:00.000Z",
"releaseTimestamp": "2021-08-23T21:03:00.000Z",
"version": "1.0.1",
},
{
"releaseTimestamp": "2021-07-21T20:09:00.000Z",
"releaseTimestamp": "2021-07-21T21:09:00.000Z",
"version": "1.0.2",
},
{
"releaseTimestamp": "2021-02-06T09:54:00.000Z",
"releaseTimestamp": "2021-02-06T10:54:00.000Z",
"version": "1.0.3",
},
],
Expand All @@ -53,22 +53,22 @@ exports[`modules/datasource/artifactory/index getReleases parses real data (merg
"releases": [
{
"registryUrl": "https://jfrog.company.com/artifactory",
"releaseTimestamp": "2021-07-21T20:08:00.000Z",
"releaseTimestamp": "2021-07-21T21:08:00.000Z",
"version": "1.0.0",
},
{
"registryUrl": "https://jfrog.company.com/artifactory",
"releaseTimestamp": "2021-08-23T20:03:00.000Z",
"releaseTimestamp": "2021-08-23T21:03:00.000Z",
"version": "1.0.1",
},
{
"registryUrl": "https://jfrog.company.com/artifactory",
"releaseTimestamp": "2021-07-21T20:09:00.000Z",
"releaseTimestamp": "2021-07-21T21:09:00.000Z",
"version": "1.0.2",
},
{
"registryUrl": "https://jfrog.company.com/artifactory",
"releaseTimestamp": "2021-02-06T09:54:00.000Z",
"releaseTimestamp": "2021-02-06T10:54:00.000Z",
"version": "1.0.3",
},
{
Expand Down
9 changes: 6 additions & 3 deletions lib/modules/platform/azure/azure-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ import { streamToString } from '../../../util/streams';
import { getNewBranchName } from '../util';
import * as azureApi from './azure-got-wrapper';
import { WrappedExceptionSchema } from './schema';
import { AzurePolicyTypeId } from './types';
import {
getBranchNameWithoutRefsPrefix,
getBranchNameWithoutRefsheadsPrefix,
} from './util';

const mergePolicyGuid = 'fa4e907d-c16b-4a4c-9dfa-4916e5d171ab'; // Magic GUID for merge strategy policy configurations

export async function getRefs(
repoId: string,
branchName?: string,
Expand Down Expand Up @@ -151,7 +150,11 @@ export async function getMergeMethod(
const policyConfigurations = (
await (
await azureApi.policyApi()
).getPolicyConfigurations(project, undefined, mergePolicyGuid)
).getPolicyConfigurations(
project,
undefined,
AzurePolicyTypeId.RequireAMergeStrategy,
)
)
.filter((p) => p.settings.scope.some(isRelevantScope))
.map((p) => p.settings)[0];
Expand Down
151 changes: 150 additions & 1 deletion lib/modules/platform/azure/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
GitStatusState,
PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces.js';
import {
type PolicyEvaluationRecord,
PolicyEvaluationStatus,
} from 'azure-devops-node-api/interfaces/PolicyInterfaces';
import { mockDeep } from 'jest-mock-extended';
import { mocked, partial } from '../../../../test/util';
import {
Expand All @@ -17,7 +21,7 @@ import type { logger as _logger } from '../../../logger';
import type * as _git from '../../../util/git';
import type * as _hostRules from '../../../util/host-rules';
import type { Platform, RepoParams } from '../types';
import { AzurePrVote } from './types';
import { AzurePolicyTypeId, AzurePrVote } from './types';

jest.mock('./azure-got-wrapper');
jest.mock('./azure-helper');
Expand Down Expand Up @@ -1898,6 +1902,151 @@ describe('modules/platform/azure/index', () => {
expect(logger.warn).toHaveBeenCalled();
expect(res).toBeTrue();
});

it('should bypass policies with provided reason if non approved policies are bypassed', async () => {
await initRepo({ repository: 'some/repo' });
const pullRequestIdMock = 12345;
const branchNameMock = 'test';
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };
const bypassReasonMock = 'Bypassed by Renovate';

const updatePullRequestMock = jest.fn(() => ({
status: PullRequestStatus.Completed,
}));
azureApi.gitApi.mockImplementationOnce(
() =>
({
getPullRequestById: jest.fn(() => ({
lastMergeSourceCommit: lastMergeSourceCommitMock,
targetRefName: 'refs/heads/ding',
title: 'title',
})),
updatePullRequest: updatePullRequestMock,
}) as any,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no any, use partial helper function

);
azureApi.policyApi.mockImplementationOnce(
() =>
({
getPolicyEvaluations: jest.fn(
() =>
[
{
configuration: {
settings: undefined,
isEnabled: true,
isBlocking: true,
type: {
id: AzurePolicyTypeId.MinimumNumberOfReviewers,
},
},
status: PolicyEvaluationStatus.Queued,
},
] satisfies PolicyEvaluationRecord[],
),
}) as any,
);

azureHelper.getMergeMethod = jest
.fn()
.mockReturnValue(GitPullRequestMergeStrategy.Squash);

const res = await azure.mergePr({
branchName: branchNameMock,
id: pullRequestIdMock,
strategy: 'auto',
platformOptions: {
azureBypassPolicyMinimumNumberOfReviewers: true,
azureBypassPolicyReason: bypassReasonMock,
},
});

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

it('should not bypass policies if enabled but some policies did not pass', async () => {
await initRepo({ repository: 'some/repo' });
const pullRequestIdMock = 12345;
const branchNameMock = 'test';
const lastMergeSourceCommitMock = { commitId: 'abcd1234' };

const updatePullRequestMock = jest.fn(() => ({
status: PullRequestStatus.Completed,
}));
azureApi.gitApi.mockImplementationOnce(
() =>
({
getPullRequestById: jest.fn(() => ({
lastMergeSourceCommit: lastMergeSourceCommitMock,
targetRefName: 'refs/heads/ding',
title: 'title',
})),
updatePullRequest: updatePullRequestMock,
}) as any,
);
azureApi.policyApi.mockImplementationOnce(
() =>
({
getPolicyEvaluations: jest.fn(
() =>
[
{
configuration: {
settings: undefined,
isEnabled: true,
isBlocking: true,
type: {
id: AzurePolicyTypeId.MinimumNumberOfReviewers,
},
},
status: PolicyEvaluationStatus.Queued,
},
] satisfies PolicyEvaluationRecord[],
),
}) as any,
);

azureHelper.getMergeMethod = jest
.fn()
.mockReturnValue(GitPullRequestMergeStrategy.Squash);

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

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

describe('deleteLabel()', () => {
Expand Down
Loading