Skip to content

Commit

Permalink
feat(platform): re-attempt platform automerge on github and gitlab (#…
Browse files Browse the repository at this point in the history
…26567)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
  • Loading branch information
straub and viceice committed Mar 18, 2024
1 parent de9608b commit ebf0c7b
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 7 deletions.
3 changes: 1 addition & 2 deletions docs/usage/configuration-options.md
Expand Up @@ -3150,8 +3150,7 @@ If enabled Renovate will pin Docker images or GitHub Actions by means of their S

If you have enabled `automerge` and set `automergeType=pr` in the Renovate config, then leaving `platformAutomerge` as `true` speeds up merging via the platform's native automerge functionality.

Renovate tries platform-native automerge only when it initially creates the PR.
Any PR that is being updated will be automerged with the Renovate-based automerge.
On GitHub and GitLab, Renovate re-enables the PR for platform-native automerge whenever it's rebased.

`platformAutomerge` will configure PRs to be merged after all (if any) branch policies have been met.
This option is available for Azure, Gitea, GitHub and GitLab.
Expand Down
129 changes: 128 additions & 1 deletion lib/modules/platform/github/index.spec.ts
Expand Up @@ -19,7 +19,12 @@ import * as _hostRules from '../../../util/host-rules';
import { setBaseUrl } from '../../../util/http/github';
import { toBase64 } from '../../../util/string';
import { hashBody } from '../pr-body';
import type { CreatePRConfig, RepoParams, UpdatePrConfig } from '../types';
import type {
CreatePRConfig,
ReattemptPlatformAutomergeConfig,
RepoParams,
UpdatePrConfig,
} from '../types';
import * as branch from './branch';
import type { ApiPageCache, GhRestPr } from './types';
import * as github from '.';
Expand Down Expand Up @@ -3330,6 +3335,128 @@ describe('modules/platform/github/index', () => {
});
});

describe('reattemptPlatformAutomerge(number, platformOptions)', () => {
const getPrListResp = [
{
number: 1234,
base: { sha: '1234' },
head: { ref: 'somebranch', repo: { full_name: 'some/repo' } },
state: 'open',
title: 'Some PR',
},
];
const getPrResp = {
number: 123,
node_id: 'abcd',
head: { repo: { full_name: 'some/repo' } },
};

const graphqlAutomergeResp = {
data: {
enablePullRequestAutoMerge: {
pullRequest: {
number: 123,
},
},
},
};

const pr: ReattemptPlatformAutomergeConfig = {
number: 123,
platformOptions: { usePlatformAutomerge: true },
};

const mockScope = async (repoOpts: any = {}): Promise<httpMock.Scope> => {
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo', repoOpts);
scope
.get(
'/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1',
)
.reply(200, getPrListResp);
scope.get('/repos/some/repo/pulls/123').reply(200, getPrResp);
await github.initRepo({ repository: 'some/repo' });
return scope;
};

const graphqlGetRepo = {
method: 'POST',
url: 'https://api.github.com/graphql',
graphql: { query: { repository: {} } },
};

const restGetPrList = {
method: 'GET',
url: 'https://api.github.com/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1',
};

const restGetPr = {
method: 'GET',
url: 'https://api.github.com/repos/some/repo/pulls/123',
};

const graphqlAutomerge = {
method: 'POST',
url: 'https://api.github.com/graphql',
graphql: {
mutation: {
__vars: {
$pullRequestId: 'ID!',
$mergeMethod: 'PullRequestMergeMethod!',
},
enablePullRequestAutoMerge: {
__args: {
input: {
pullRequestId: '$pullRequestId',
mergeMethod: '$mergeMethod',
},
},
},
},
variables: {
pullRequestId: 'abcd',
mergeMethod: 'REBASE',
},
},
};

it('should set automatic merge', async () => {
const scope = await mockScope();
scope.post('/graphql').reply(200, graphqlAutomergeResp);

await expect(github.reattemptPlatformAutomerge(pr)).toResolve();

expect(logger.logger.debug).toHaveBeenLastCalledWith(
'PR platform automerge re-attempted...prNo: 123',
);

expect(httpMock.getTrace()).toMatchObject([
graphqlGetRepo,
restGetPrList,
restGetPr,
graphqlAutomerge,
]);
});

it('handles unknown error', async () => {
const scope = httpMock.scope(githubApiHost);
initRepoMock(scope, 'some/repo');
await github.initRepo({ repository: 'some/repo' });
scope
.get(
'/repos/some/repo/pulls?per_page=100&state=all&sort=updated&direction=desc&page=1',
)
.replyWithError('unknown error');

await expect(github.reattemptPlatformAutomerge(pr)).toResolve();

expect(logger.logger.warn).toHaveBeenCalledWith(
{ err: new Error('external-host-error') },
'Error re-attempting PR platform automerge',
);
});
});

describe('mergePr(prNo)', () => {
it('should merge the PR', async () => {
const scope = httpMock.scope(githubApiHost);
Expand Down
17 changes: 17 additions & 0 deletions lib/modules/platform/github/index.ts
Expand Up @@ -60,6 +60,7 @@ import type {
PlatformParams,
PlatformPrOptions,
PlatformResult,
ReattemptPlatformAutomergeConfig,
RepoParams,
RepoResult,
UpdatePrConfig,
Expand Down Expand Up @@ -1794,6 +1795,22 @@ export async function updatePr({
}
}

export async function reattemptPlatformAutomerge({
number,
platformOptions,
}: ReattemptPlatformAutomergeConfig): Promise<void> {
try {
const result = (await getPr(number))!;
const { node_id } = result;

await tryPrAutomerge(number, node_id, platformOptions);

logger.debug(`PR platform automerge re-attempted...prNo: ${number}`);
} catch (err) {
logger.warn({ err }, 'Error re-attempting PR platform automerge');
}
}

export async function mergePr({
branchName,
id: prNo,
Expand Down
32 changes: 32 additions & 0 deletions lib/modules/platform/gitlab/index.spec.ts
Expand Up @@ -2876,6 +2876,38 @@ describe('modules/platform/gitlab/index', () => {
});
});

describe('reattemptPlatformAutomerge(number, platformOptions)', () => {
const pr = {
number: 12345,
platformOptions: {
usePlatformAutomerge: true,
},
};

it('should set automatic merge', async () => {
await initPlatform('13.3.6-ee');
httpMock
.scope(gitlabApiHost)
.get('/api/v4/projects/undefined/merge_requests/12345')
.reply(200)
.get('/api/v4/projects/undefined/merge_requests/12345')
.reply(200, {
merge_status: 'can_be_merged',
pipeline: {
status: 'running',
},
})
.put('/api/v4/projects/undefined/merge_requests/12345/merge')
.reply(200);

await expect(gitlab.reattemptPlatformAutomerge?.(pr)).toResolve();

expect(logger.debug).toHaveBeenLastCalledWith(
'PR platform automerge re-attempted...prNo: 12345',
);
});
});

describe('mergePr(pr)', () => {
it('merges the PR', async () => {
httpMock
Expand Down
8 changes: 8 additions & 0 deletions lib/modules/platform/gitlab/index.ts
Expand Up @@ -48,6 +48,7 @@ import type {
PlatformPrOptions,
PlatformResult,
Pr,
ReattemptPlatformAutomergeConfig,
RepoParams,
RepoResult,
UpdatePrConfig,
Expand Down Expand Up @@ -848,8 +849,15 @@ export async function updatePr({
if (platformOptions?.autoApprove) {
await approvePr(iid);
}
}

export async function reattemptPlatformAutomerge({
number: iid,
platformOptions,
}: ReattemptPlatformAutomergeConfig): Promise<void> {
await tryPrAutomerge(iid, platformOptions);

logger.debug(`PR platform automerge re-attempted...prNo: ${iid}`);
}

export async function mergePr({ id }: MergePRConfig): Promise<boolean> {
Expand Down
7 changes: 7 additions & 0 deletions lib/modules/platform/types.ts
Expand Up @@ -123,6 +123,10 @@ export interface UpdatePrConfig {
state?: 'open' | 'closed';
targetBranch?: string;
}
export interface ReattemptPlatformAutomergeConfig {
number: number;
platformOptions?: PlatformPrOptions;
}
export interface EnsureIssueConfig {
title: string;
reuseTitle?: string;
Expand Down Expand Up @@ -226,6 +230,9 @@ export interface Platform {
getPr(number: number): Promise<Pr | null>;
findPr(findPRConfig: FindPRConfig): Promise<Pr | null>;
refreshPr?(number: number): Promise<void>;
reattemptPlatformAutomerge?(
prConfig: ReattemptPlatformAutomergeConfig,
): Promise<void>;
getBranchStatus(
branchName: string,
internalChecksAsSuccess: boolean,
Expand Down
4 changes: 4 additions & 0 deletions lib/workers/repository/update/branch/index.spec.ts
Expand Up @@ -108,6 +108,7 @@ describe('workers/repository/update/branch/index', () => {
beforeEach(() => {
scm.branchExists.mockResolvedValue(false);
prWorker.ensurePr = jest.fn();
prWorker.getPlatformPrOptions = jest.fn();
prAutomerge.checkAutoMerge = jest.fn();
// TODO: incompatible types (#22198)
config = {
Expand All @@ -133,6 +134,9 @@ describe('workers/repository/update/branch/index', () => {
state: '',
}),
});
prWorker.getPlatformPrOptions.mockReturnValue({
usePlatformAutomerge: true,
});
GlobalConfig.set(adminConfig);
// TODO: fix types, jest is using wrong overload (#22198)
sanitize.sanitize.mockImplementation((input) => input!);
Expand Down
21 changes: 17 additions & 4 deletions lib/workers/repository/update/branch/index.ts
Expand Up @@ -36,7 +36,7 @@ import * as template from '../../../../util/template';
import { isLimitReached } from '../../../global/limits';
import type { BranchConfig, BranchResult, PrBlockedBy } from '../../../types';
import { embedChangelogs } from '../../changelog';
import { ensurePr } from '../pr';
import { ensurePr, getPlatformPrOptions } from '../pr';
import { checkAutoMerge } from '../pr/automerge';
import { setArtifactErrorStatus } from './artifacts';
import { tryBranchAutomerge } from './automerge';
Expand Down Expand Up @@ -572,9 +572,22 @@ export async function processBranch(
await scm.checkoutBranch(config.baseBranch);
updatesVerified = true;
}
// istanbul ignore if
if (branchPr && platform.refreshPr) {
await platform.refreshPr(branchPr.number);

if (branchPr) {
const platformOptions = getPlatformPrOptions(config);
if (
platformOptions.usePlatformAutomerge &&
platform.reattemptPlatformAutomerge
) {
await platform.reattemptPlatformAutomerge({
number: branchPr.number,
platformOptions,
});
}
// istanbul ignore if
if (platform.refreshPr) {
await platform.refreshPr(branchPr.number);
}
}
if (!commitSha && !branchExists) {
return {
Expand Down

0 comments on commit ebf0c7b

Please sign in to comment.