diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 4d166ae3ead919..d289ca77023e59 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -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. diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts index d0a33bb4ea4681..995b2091f1de7a 100644 --- a/lib/modules/platform/github/index.spec.ts +++ b/lib/modules/platform/github/index.spec.ts @@ -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 '.'; @@ -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 => { + 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); diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts index e629dd6e6e6b43..def3a90e63b194 100644 --- a/lib/modules/platform/github/index.ts +++ b/lib/modules/platform/github/index.ts @@ -60,6 +60,7 @@ import type { PlatformParams, PlatformPrOptions, PlatformResult, + ReattemptPlatformAutomergeConfig, RepoParams, RepoResult, UpdatePrConfig, @@ -1794,6 +1795,22 @@ export async function updatePr({ } } +export async function reattemptPlatformAutomerge({ + number, + platformOptions, +}: ReattemptPlatformAutomergeConfig): Promise { + 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, diff --git a/lib/modules/platform/gitlab/index.spec.ts b/lib/modules/platform/gitlab/index.spec.ts index 676df2e47ef18f..1cf1984c01b36d 100644 --- a/lib/modules/platform/gitlab/index.spec.ts +++ b/lib/modules/platform/gitlab/index.spec.ts @@ -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 diff --git a/lib/modules/platform/gitlab/index.ts b/lib/modules/platform/gitlab/index.ts index a84b83868c0cf7..263c51c25b763c 100644 --- a/lib/modules/platform/gitlab/index.ts +++ b/lib/modules/platform/gitlab/index.ts @@ -48,6 +48,7 @@ import type { PlatformPrOptions, PlatformResult, Pr, + ReattemptPlatformAutomergeConfig, RepoParams, RepoResult, UpdatePrConfig, @@ -848,8 +849,15 @@ export async function updatePr({ if (platformOptions?.autoApprove) { await approvePr(iid); } +} +export async function reattemptPlatformAutomerge({ + number: iid, + platformOptions, +}: ReattemptPlatformAutomergeConfig): Promise { await tryPrAutomerge(iid, platformOptions); + + logger.debug(`PR platform automerge re-attempted...prNo: ${iid}`); } export async function mergePr({ id }: MergePRConfig): Promise { diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index b6f75f2a58588f..156dc63015d0b5 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -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; @@ -226,6 +230,9 @@ export interface Platform { getPr(number: number): Promise; findPr(findPRConfig: FindPRConfig): Promise; refreshPr?(number: number): Promise; + reattemptPlatformAutomerge?( + prConfig: ReattemptPlatformAutomergeConfig, + ): Promise; getBranchStatus( branchName: string, internalChecksAsSuccess: boolean, diff --git a/lib/workers/repository/update/branch/index.spec.ts b/lib/workers/repository/update/branch/index.spec.ts index 25e4511fa8d17a..e5f17516c6bb98 100644 --- a/lib/workers/repository/update/branch/index.spec.ts +++ b/lib/workers/repository/update/branch/index.spec.ts @@ -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 = { @@ -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!); diff --git a/lib/workers/repository/update/branch/index.ts b/lib/workers/repository/update/branch/index.ts index 6e23c8f6c7f94a..762d55622c86d4 100644 --- a/lib/workers/repository/update/branch/index.ts +++ b/lib/workers/repository/update/branch/index.ts @@ -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'; @@ -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 {