From 46f8eed01fc5e7a822e2855720612f3e165646c2 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Fri, 29 Nov 2024 19:42:54 -0300 Subject: [PATCH 01/18] fix(gerrit): not auto-approving if change already had a Code-Review +1 --- lib/modules/platform/gerrit/client.spec.ts | 89 ++++++++++++++++++---- lib/modules/platform/gerrit/client.ts | 32 +++++--- lib/modules/platform/gerrit/scm.spec.ts | 1 + lib/modules/platform/gerrit/scm.ts | 2 +- lib/modules/platform/gerrit/types.ts | 7 ++ 5 files changed, 103 insertions(+), 28 deletions(-) diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index 2d5ff5cddbf9fe..72073d96bb1912 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -393,10 +393,20 @@ describe('modules/platform/gerrit/client', () => { describe('approveChange()', () => { it('already approved - do nothing', async () => { - const change = partial<GerritChange>({}); + const change = partial<GerritChange>({ + labels: { + 'Code-Review': { + all: [{ value: 2 }], + }, + }, + }); httpMock .scope(gerritEndpointUrl) - .get((url) => url.includes('/a/changes/123456?o=')) + .get( + (url) => + url.includes('/a/changes/123456?o=') && + url.includes('o=DETAILED_LABELS'), + ) .reply(200, gerritRestResponse(change), jsonResultHeader); await expect(client.approveChange(123456)).toResolve(); }); @@ -405,17 +415,53 @@ describe('modules/platform/gerrit/client', () => { const change = partial<GerritChange>({ labels: {} }); httpMock .scope(gerritEndpointUrl) - .get((url) => url.includes('/a/changes/123456?o=')) + .get( + (url) => + url.includes('/a/changes/123456?o=') && + url.includes('o=DETAILED_LABELS'), + ) .reply(200, gerritRestResponse(change), jsonResultHeader); await expect(client.approveChange(123456)).toResolve(); }); it('not already approved - approve now', async () => { - const change = partial<GerritChange>({ labels: { 'Code-Review': {} } }); + const change = partial<GerritChange>({ + labels: { 'Code-Review': { all: [] } }, + }); + httpMock + .scope(gerritEndpointUrl) + .get( + (url) => + url.includes('/a/changes/123456?o=') && + url.includes('o=DETAILED_LABELS'), + ) + .reply(200, gerritRestResponse(change), jsonResultHeader); + const approveMock = httpMock + .scope(gerritEndpointUrl) + .post('/a/changes/123456/revisions/current/review', { + labels: { 'Code-Review': +2 }, + }) + .reply(200, gerritRestResponse(''), jsonResultHeader); + await expect(client.approveChange(123456)).toResolve(); + expect(approveMock.isDone()).toBeTrue(); + }); + + it('not already approved because of +1 - approve now', async () => { + const change = partial<GerritChange>({ + labels: { + 'Code-Review': { + all: [{ value: 1 }], + }, + }, + }); httpMock .scope(gerritEndpointUrl) - .get((url) => url.includes('/a/changes/123456?o=')) + .get( + (url) => + url.includes('/a/changes/123456?o=') && + url.includes('o=DETAILED_LABELS'), + ) .reply(200, gerritRestResponse(change), jsonResultHeader); const approveMock = httpMock .scope(gerritEndpointUrl) @@ -432,7 +478,7 @@ describe('modules/platform/gerrit/client', () => { it('label not exists', () => { expect( client.wasApprovedBy(partial<GerritChange>({}), 'user'), - ).toBeUndefined(); + ).toBeFalse(); }); it('not approved by anyone', () => { @@ -440,12 +486,29 @@ describe('modules/platform/gerrit/client', () => { client.wasApprovedBy( partial<GerritChange>({ labels: { - 'Code-Review': {}, + 'Code-Review': { + all: [], + }, + }, + }), + 'user', + ), + ).toBeFalse(); + }); + + it('not approved but with +1', () => { + expect( + client.wasApprovedBy( + partial<GerritChange>({ + labels: { + 'Code-Review': { + all: [{ value: 1, username: 'user' }], + }, }, }), 'user', ), - ).toBeUndefined(); + ).toBeFalse(); }); it('approved by given user', () => { @@ -454,10 +517,7 @@ describe('modules/platform/gerrit/client', () => { partial<GerritChange>({ labels: { 'Code-Review': { - approved: { - _account_id: 1, - username: 'user', - }, + all: [{ value: 2, username: 'user' }], }, }, }), @@ -472,10 +532,7 @@ describe('modules/platform/gerrit/client', () => { partial<GerritChange>({ labels: { 'Code-Review': { - approved: { - _account_id: 1, - username: 'other', - }, + all: [{ value: 2, username: 'other' }], }, }, }), diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index 89b4ebf3e66e3c..efe7081ffc9f74 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -58,12 +58,14 @@ class GerritClient { repository: string, findPRConfig: GerritFindPRConfig, refreshCache?: boolean, + extraOptions?: string[], ): Promise<GerritChange[]> { const filters = GerritClient.buildSearchFilters(repository, findPRConfig); + const options = [...this.requestDetails, ...(extraOptions ?? [])]; const changes = await this.gerritHttp.getJson<GerritChange[]>( `a/changes/?q=` + filters.join('+') + - this.requestDetails.map((det) => `&o=${det}`).join(''), + options.map((det) => `&o=${det}`).join(''), { memCache: !refreshCache }, ); logger.trace( @@ -72,10 +74,13 @@ class GerritClient { return changes.body; } - async getChange(changeNumber: number): Promise<GerritChange> { + async getChange( + changeNumber: number, + extraOptions?: string[], + ): Promise<GerritChange> { + const options = [...this.requestDetails, ...(extraOptions ?? [])]; const changes = await this.gerritHttp.getJson<GerritChange>( - `a/changes/${changeNumber}?` + - this.requestDetails.map((det) => `o=${det}`).join('&'), + `a/changes/${changeNumber}?` + options.map((det) => `o=${det}`).join('&'), ); return changes.body; } @@ -189,15 +194,20 @@ class GerritClient { } async checkIfApproved(changeId: number): Promise<boolean> { - const change = await client.getChange(changeId); - const reviewLabels = change?.labels?.['Code-Review']; - return reviewLabels === undefined || reviewLabels.approved !== undefined; + const change = await client.getChange(changeId, ['DETAILED_LABELS']); + const reviewLabel = change?.labels?.['Code-Review']; + return ( + reviewLabel === undefined || + Boolean(reviewLabel.all?.some((label) => label.value === 2)) + ); } - wasApprovedBy(change: GerritChange, username: string): boolean | undefined { - return ( - change.labels?.['Code-Review'].approved && - change.labels['Code-Review'].approved.username === username + wasApprovedBy(change: GerritChange, username: string): boolean { + const reviewLabel = change?.labels?.['Code-Review']; + return Boolean( + reviewLabel?.all?.some( + (label) => label.value === 2 && label.username === username, + ), ); } diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index f2041ef7f365c4..b654183fafd506 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -277,6 +277,7 @@ describe('modules/platform/gerrit/scm', () => { targetBranch: 'main', }, true, + ['DETAILED_LABELS'], ); }); diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index 6289f1f1f24047..646e975f1d93a8 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -101,7 +101,7 @@ export class GerritScm extends DefaultGitScm { targetBranch: commit.baseBranch, }; const existingChange = await client - .findChanges(repository, searchConfig, true) + .findChanges(repository, searchConfig, true, ['DETAILED_LABELS']) .then((res) => res.pop()); let hasChanges = true; diff --git a/lib/modules/platform/gerrit/types.ts b/lib/modules/platform/gerrit/types.ts index 0d1b5d90fed354..7f8603d275f11b 100644 --- a/lib/modules/platform/gerrit/types.ts +++ b/lib/modules/platform/gerrit/types.ts @@ -77,6 +77,8 @@ export interface GerritChangeMessageInfo { export interface GerritLabelInfo { approved?: GerritAccountInfo; rejected?: GerritAccountInfo; + /** List of votes. Only set when o=DETAILED_LABELS. */ + all?: GerritApprovalInfo[]; } export interface GerritActionInfo { @@ -99,3 +101,8 @@ export interface GerritMergeableInfo { | 'CHERRY_PICK'; mergeable: boolean; } + +export interface GerritApprovalInfo { + value?: number; + username?: string; +} From 107a298054e91b67c3cbb68cff8a713ce546281a Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sat, 30 Nov 2024 01:48:22 +0000 Subject: [PATCH 02/18] chore(gerrit): improve auto-approve on new patchset workaround --- lib/modules/platform/gerrit/client.spec.ts | 68 ------------------- lib/modules/platform/gerrit/client.ts | 13 +--- lib/modules/platform/gerrit/scm.spec.ts | 8 +-- lib/modules/platform/gerrit/scm.ts | 9 +-- lib/util/git/types.ts | 2 + .../repository/update/branch/commit.ts | 2 + 6 files changed, 10 insertions(+), 92 deletions(-) diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index 72073d96bb1912..b2ddb8775cb96a 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -473,74 +473,6 @@ describe('modules/platform/gerrit/client', () => { expect(approveMock.isDone()).toBeTrue(); }); }); - - describe('wasApprovedBy()', () => { - it('label not exists', () => { - expect( - client.wasApprovedBy(partial<GerritChange>({}), 'user'), - ).toBeFalse(); - }); - - it('not approved by anyone', () => { - expect( - client.wasApprovedBy( - partial<GerritChange>({ - labels: { - 'Code-Review': { - all: [], - }, - }, - }), - 'user', - ), - ).toBeFalse(); - }); - - it('not approved but with +1', () => { - expect( - client.wasApprovedBy( - partial<GerritChange>({ - labels: { - 'Code-Review': { - all: [{ value: 1, username: 'user' }], - }, - }, - }), - 'user', - ), - ).toBeFalse(); - }); - - it('approved by given user', () => { - expect( - client.wasApprovedBy( - partial<GerritChange>({ - labels: { - 'Code-Review': { - all: [{ value: 2, username: 'user' }], - }, - }, - }), - 'user', - ), - ).toBeTrue(); - }); - - it('approved by given other', () => { - expect( - client.wasApprovedBy( - partial<GerritChange>({ - labels: { - 'Code-Review': { - all: [{ value: 2, username: 'other' }], - }, - }, - }), - 'user', - ), - ).toBeFalse(); - }); - }); }); function gerritRestResponse(body: any): any { diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index efe7081ffc9f74..96d68e985ac365 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -58,14 +58,12 @@ class GerritClient { repository: string, findPRConfig: GerritFindPRConfig, refreshCache?: boolean, - extraOptions?: string[], ): Promise<GerritChange[]> { const filters = GerritClient.buildSearchFilters(repository, findPRConfig); - const options = [...this.requestDetails, ...(extraOptions ?? [])]; const changes = await this.gerritHttp.getJson<GerritChange[]>( `a/changes/?q=` + filters.join('+') + - options.map((det) => `&o=${det}`).join(''), + this.requestDetails.map((det) => `&o=${det}`).join(''), { memCache: !refreshCache }, ); logger.trace( @@ -202,15 +200,6 @@ class GerritClient { ); } - wasApprovedBy(change: GerritChange, username: string): boolean { - const reviewLabel = change?.labels?.['Code-Review']; - return Boolean( - reviewLabel?.all?.some( - (label) => label.value === 2 && label.username === username, - ), - ); - } - normalizeMessage(message: string): string { //the last \n was removed from gerrit after the comment was added... return message.substring(0, 0x4000).trim(); diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index b654183fafd506..43df55d3b8d783 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -277,7 +277,6 @@ describe('modules/platform/gerrit/scm', () => { targetBranch: 'main', }, true, - ['DETAILED_LABELS'], ); }); @@ -370,7 +369,6 @@ describe('modules/platform/gerrit/scm', () => { }, }); clientMock.findChanges.mockResolvedValueOnce([existingChange]); - clientMock.wasApprovedBy.mockReturnValueOnce(true); git.prepareCommit.mockResolvedValueOnce({ commitSha: 'commitSha' as LongCommitSha, parentCommitSha: 'parentSha' as LongCommitSha, @@ -386,6 +384,7 @@ describe('modules/platform/gerrit/scm', () => { message: 'commit msg', files: [], prTitle: 'pr title', + autoApprove: true, }), ).toBe('commitSha'); expect(git.prepareCommit).toHaveBeenCalledWith({ @@ -397,6 +396,7 @@ describe('modules/platform/gerrit/scm', () => { 'Renovate-Branch: renovate/dependency-1.x\nChange-Id: ...', ], prTitle: 'pr title', + autoApprove: true, force: true, }); expect(git.fetchRevSpec).toHaveBeenCalledWith('refs/changes/1/2'); @@ -405,10 +405,6 @@ describe('modules/platform/gerrit/scm', () => { sourceRef: 'renovate/dependency-1.x', targetRef: 'refs/for/main', }); - expect(clientMock.wasApprovedBy).toHaveBeenCalledWith( - existingChange, - 'user', - ); expect(clientMock.approveChange).toHaveBeenCalledWith(123456); }); }); diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index 646e975f1d93a8..3e1e9672f7a55a 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -101,7 +101,7 @@ export class GerritScm extends DefaultGitScm { targetBranch: commit.baseBranch, }; const existingChange = await client - .findChanges(repository, searchConfig, true, ['DETAILED_LABELS']) + .findChanges(repository, searchConfig, true) .then((res) => res.pop()); let hasChanges = true; @@ -135,11 +135,8 @@ export class GerritScm extends DefaultGitScm { files: commit.files, }); if (pushResult) { - //existingChange was the old change before commit/push. we need to approve again, if it was previously approved from renovate - if ( - existingChange && - client.wasApprovedBy(existingChange, username) - ) { + // New patchsets dismisses the approval, so we need to approve it again + if (existingChange && commit.autoApprove) { await client.approveChange(existingChange._number); } return commitSha; diff --git a/lib/util/git/types.ts b/lib/util/git/types.ts index 63544838059dfb..d4cb37b6118ae2 100644 --- a/lib/util/git/types.ts +++ b/lib/util/git/types.ts @@ -85,6 +85,8 @@ export interface CommitFilesConfig { platformCommit?: PlatformCommitOptions; /** Only needed by Gerrit platform */ prTitle?: string; + /** Only needed by Gerrit platform */ + autoApprove?: boolean; } export interface PushFilesConfig { diff --git a/lib/workers/repository/update/branch/commit.ts b/lib/workers/repository/update/branch/commit.ts index cadafc9ca0e75c..cf7c118ada226f 100644 --- a/lib/workers/repository/update/branch/commit.ts +++ b/lib/workers/repository/update/branch/commit.ts @@ -62,5 +62,7 @@ export function commitFilesToBranch( platformCommit: config.platformCommit, // Only needed by Gerrit platform prTitle: config.prTitle, + // Only needed by Gerrit platform + autoApprove: config.autoApprove, }); } From 6ac7b9e0896ace6ec29b846f26f882f5e46d172d Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sat, 30 Nov 2024 04:04:20 -0300 Subject: [PATCH 03/18] chore(gerrit): try auto-approve right before auto-merge --- lib/modules/platform/gerrit/index.ts | 4 ++++ lib/modules/platform/types.ts | 1 + lib/workers/repository/update/pr/automerge.ts | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index d4f5b3c8123780..43133bd9250304 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -436,3 +436,7 @@ export function findIssue(title: string): Promise<Issue | null> { export function getIssueList(): Promise<Issue[]> { return Promise.resolve([]); } + +export async function approvePr(number: number): Promise<void> { + await client.approveChange(number); +} diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index cc770c1d8e43ee..67e95722462fa4 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -282,6 +282,7 @@ export interface Platform { maxBodyLength(): number; labelCharLimit?(): number; + approvePr?(number: number): Promise<void>; } export interface PlatformScm { diff --git a/lib/workers/repository/update/pr/automerge.ts b/lib/workers/repository/update/pr/automerge.ts index 2ace410741b3b8..4bbe2aaab1d53e 100644 --- a/lib/workers/repository/update/pr/automerge.ts +++ b/lib/workers/repository/update/pr/automerge.ts @@ -69,6 +69,12 @@ export async function checkAutoMerge( prAutomergeBlockReason: 'PlatformNotReady', }; } + // Usually the PR will already be approved, this is a last resort in case the + // approval was lost for some reason + if (config.autoApprove && platform.approvePr) { + logger.debug('Auto-approving PR if needed before automerge'); + await platform.approvePr(pr.number); + } const branchStatus = await resolveBranchStatus( branchName, !!config.internalChecksAsSuccess, From 1d8daf50953601b93aecaa3838ecdd984a296978 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sun, 1 Dec 2024 16:40:11 +0000 Subject: [PATCH 04/18] test(gerrit): fix approvePr coverage --- lib/modules/platform/gerrit/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index 0d3989e148757b..a3246f5da38aa0 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -161,7 +161,7 @@ export async function updatePr(prConfig: UpdatePrConfig): Promise<void> { ); } if (prConfig.platformPrOptions?.autoApprove) { - await client.approveChange(prConfig.number); + await approvePr(prConfig.number); } if (prConfig.state && prConfig.state === 'closed') { await client.abandonChange(prConfig.number); @@ -196,7 +196,7 @@ export async function createPr(prConfig: CreatePRConfig): Promise<Pr | null> { TAG_PULL_REQUEST_BODY, ); if (prConfig.platformPrOptions?.autoApprove) { - await client.approveChange(pr._number); + await approvePr(pr._number); } return getPr(pr._number); } From 5eb284603f3afdcb0223d036c9d029482071e46b Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sun, 1 Dec 2024 17:02:52 +0000 Subject: [PATCH 05/18] chore(gerrit): still approve if change was approved another user --- lib/modules/platform/gerrit/client.spec.ts | 42 +++++++++++++++++++--- lib/modules/platform/gerrit/client.ts | 13 +++++-- lib/modules/platform/gerrit/types.ts | 1 + 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index a451bd020d9d46..7ac3193a68262d 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -5,10 +5,11 @@ import { setBaseUrl } from '../../../util/http/gerrit'; import type { FindPRConfig } from '../types'; import { client } from './client'; import type { - GerritChange, - GerritChangeMessageInfo, - GerritFindPRConfig, - GerritMergeableInfo, + GerritAccountInfo, + type GerritChange, + type GerritChangeMessageInfo, + type GerritFindPRConfig, + type GerritMergeableInfo, } from './types'; const gerritEndpointUrl = 'https://dev.gerrit.com/renovate/'; @@ -399,12 +400,14 @@ describe('modules/platform/gerrit/client', () => { describe('approveChange()', () => { it('already approved - do nothing', async () => { + const owner = partial<GerritAccountInfo>({ username: 'user' }); const change = partial<GerritChange>({ labels: { 'Code-Review': { - all: [{ value: 2 }], + all: [{ value: 2, username: owner.username }], }, }, + owner, }); httpMock .scope(gerritEndpointUrl) @@ -480,6 +483,35 @@ describe('modules/platform/gerrit/client', () => { await expect(client.approveChange(123456)).toResolve(); expect(approveMock.isDone()).toBeTrue(); }); + + it('already approved by another user - approve again', async () => { + const owner = partial<GerritAccountInfo>({ username: 'user' }); + const change = partial<GerritChange>({ + labels: { + 'Code-Review': { + all: [{ value: 2, username: 'other user' }], + }, + }, + owner, + }); + httpMock + .scope(gerritEndpointUrl) + .get( + (url) => + url.includes('/a/changes/123456?o=') && + url.includes('o=DETAILED_LABELS'), + ) + .reply(200, gerritRestResponse(change), jsonResultHeader); + const approveMock = httpMock + .scope(gerritEndpointUrl) + .post('/a/changes/123456/revisions/current/review', { + labels: { 'Code-Review': +2 }, + notify: 'NONE', + }) + .reply(200, gerritRestResponse(''), jsonResultHeader); + await expect(client.approveChange(123456)).toResolve(); + expect(approveMock.isDone()).toBeTrue(); + }); }); }); diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index 83b82454ad141c..74221a497da71c 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -193,9 +193,11 @@ class GerritClient { async approveChange(changeId: number): Promise<void> { const isApproved = await this.checkIfApproved(changeId); - if (!isApproved) { - await this.setLabel(changeId, 'Code-Review', +2); + if (isApproved) { + logger.debug(`Change is already approved`); + return; } + await this.setLabel(changeId, 'Code-Review', +2); } async checkIfApproved(changeId: number): Promise<boolean> { @@ -203,7 +205,12 @@ class GerritClient { const reviewLabel = change?.labels?.['Code-Review']; return ( reviewLabel === undefined || - Boolean(reviewLabel.all?.some((label) => label.value === 2)) + Boolean( + reviewLabel.all?.some( + (label) => + label.value === 2 && label.username === change.owner.username, + ), + ) ); } diff --git a/lib/modules/platform/gerrit/types.ts b/lib/modules/platform/gerrit/types.ts index b65038100b3182..9cf35154a59468 100644 --- a/lib/modules/platform/gerrit/types.ts +++ b/lib/modules/platform/gerrit/types.ts @@ -52,6 +52,7 @@ export interface GerritChange { */ revisions: Record<string, GerritRevisionInfo>; problems: unknown[]; + owner: GerritAccountInfo; } export interface GerritCommitInfo { From c679c28d01c8513adee1feea63ef4b3a53491270 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sun, 1 Dec 2024 16:38:36 +0000 Subject: [PATCH 06/18] feat(gitlab,azure): try approving before auto-merge --- .../azure/__snapshots__/index.spec.ts.snap | 4 + lib/modules/platform/azure/index.spec.ts | 80 +++++++++++- lib/modules/platform/azure/index.ts | 61 +++++---- lib/modules/platform/gitlab/index.spec.ts | 120 +++++++++++++++++- lib/modules/platform/gitlab/index.ts | 41 ++++-- lib/modules/platform/gitlab/types.ts | 4 + 6 files changed, 272 insertions(+), 38 deletions(-) diff --git a/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap b/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap index e782de620ace85..4fed47efa74ab4 100644 --- a/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap +++ b/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap @@ -217,6 +217,10 @@ exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOpti ] `; +exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOptions) should not re-approve if the PR is already approved 1`] = `undefined`; + +exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOptions) should re-approve if the PR was approved by another user 1`] = `undefined`; + exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOptions) should re-approve the PR 1`] = `undefined`; exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOptions) should reopen the PR 1`] = ` diff --git a/lib/modules/platform/azure/index.spec.ts b/lib/modules/platform/azure/index.spec.ts index 73308cdfbfa98e..efc394cac94843 100644 --- a/lib/modules/platform/azure/index.spec.ts +++ b/lib/modules/platform/azure/index.spec.ts @@ -1149,7 +1149,7 @@ describe('modules/platform/azure/index', () => { const updateFn = jest .fn(() => prUpdateResult) .mockName('createPullRequestReviewer'); - azureApi.gitApi.mockImplementationOnce( + azureApi.gitApi.mockImplementation( () => ({ createPullRequest: jest.fn(() => prResult), @@ -1293,7 +1293,82 @@ describe('modules/platform/azure/index', () => { isRequired: false, }; const updateFn = jest.fn(() => prUpdateResult); - azureApi.gitApi.mockImplementationOnce( + azureApi.gitApi.mockImplementation( + () => + ({ + updatePullRequest: jest.fn(() => prResult), + createPullRequestReviewer: updateFn, + getPullRequestById: jest.fn(() => ({ + pullRequestId: prResult.pullRequestId, + createdBy: prResult.createdBy, + })), + }) as any, + ); + const pr = await azure.updatePr({ + number: prResult.pullRequestId, + prTitle: 'The Title', + prBody: 'Hello world', + platformPrOptions: { autoApprove: true }, + }); + expect(updateFn).toHaveBeenCalled(); + expect(pr).toMatchSnapshot(); + }); + + it('should not re-approve if the PR is already approved', async () => { + await initRepo({ repository: 'some/repo' }); + const prResult = { + pullRequestId: 456, + createdBy: { + id: 123, + url: 'user-url', + }, + }; + const prUpdateResult = { + reviewerUrl: prResult.createdBy.url, + vote: AzurePrVote.Approved, + isFlagged: false, + isRequired: false, + }; + const updateFn = jest.fn(() => prUpdateResult); + azureApi.gitApi.mockImplementation( + () => + ({ + updatePullRequest: jest.fn(() => prResult), + createPullRequestReviewer: updateFn, + getPullRequestById: jest.fn(() => ({ + pullRequestId: prResult.pullRequestId, + createdBy: prResult.createdBy, + reviewers: [{ vote: AzurePrVote.Approved, id: 123 }], + })), + }) as any, + ); + const pr = await azure.updatePr({ + number: prResult.pullRequestId, + prTitle: 'The Title', + prBody: 'Hello world', + platformPrOptions: { autoApprove: true }, + }); + expect(updateFn).not.toHaveBeenCalled(); + expect(pr).toMatchSnapshot(); + }); + + it('should re-approve if the PR was approved by another user', async () => { + await initRepo({ repository: 'some/repo' }); + const prResult = { + pullRequestId: 456, + createdBy: { + id: 123, + url: 'user-url', + }, + }; + const prUpdateResult = { + reviewerUrl: prResult.createdBy.url, + vote: AzurePrVote.Approved, + isFlagged: false, + isRequired: false, + }; + const updateFn = jest.fn(() => prUpdateResult); + azureApi.gitApi.mockImplementation( () => ({ updatePullRequest: jest.fn(() => prResult), @@ -1301,6 +1376,7 @@ describe('modules/platform/azure/index', () => { getPullRequestById: jest.fn(() => ({ pullRequestId: prResult.pullRequestId, createdBy: prResult.createdBy, + reviewers: [{ vote: AzurePrVote.Approved, id: 321 }], })), }) as any, ); diff --git a/lib/modules/platform/azure/index.ts b/lib/modules/platform/azure/index.ts index 21bd44eaa3f58c..039e7e18f7badc 100644 --- a/lib/modules/platform/azure/index.ts +++ b/lib/modules/platform/azure/index.ts @@ -513,18 +513,7 @@ export async function createPr({ ); } if (platformPrOptions?.autoApprove) { - await azureApiGit.createPullRequestReviewer( - { - reviewerUrl: pr.createdBy!.url, - vote: AzurePrVote.Approved, - isFlagged: false, - isRequired: false, - }, - config.repoId, - // TODO #22198 - pr.pullRequestId!, - pr.createdBy!.id!, - ); + await approvePr(pr); } await Promise.all( labels!.map((label) => @@ -581,19 +570,7 @@ export async function updatePr({ objToUpdate.status = PullRequestStatus.Abandoned; } if (platformPrOptions?.autoApprove) { - const pr = await azureApiGit.getPullRequestById(prNo, config.project); - await azureApiGit.createPullRequestReviewer( - { - reviewerUrl: pr.createdBy!.url, - vote: AzurePrVote.Approved, - isFlagged: false, - isRequired: false, - }, - config.repoId, - // TODO #22198 - pr.pullRequestId!, - pr.createdBy!.id!, - ); + await approvePr(prNo); } const updatedPr = await azureApiGit.updatePullRequest( @@ -1015,3 +992,37 @@ export async function deleteLabel( const azureApiGit = await azureApi.gitApi(); await azureApiGit.deletePullRequestLabels(config.repoId, prNumber, label); } + +export async function approvePr( + prNumberOrPr: number | GitPullRequest, +): Promise<void> { + const azureApiGit = await azureApi.gitApi(); + const pr = + typeof prNumberOrPr === 'number' + ? await azureApiGit.getPullRequestById(prNumberOrPr, config.project) + : prNumberOrPr; + + const isApproved = pr.reviewers?.some( + (reviewer) => + reviewer.vote === AzurePrVote.Approved && + reviewer.id === pr.createdBy?.id, + ); + + if (isApproved) { + logger.debug('PR is already approved'); + return; + } + + await azureApiGit.createPullRequestReviewer( + { + reviewerUrl: pr.createdBy!.url, + vote: AzurePrVote.Approved, + isFlagged: false, + isRequired: false, + }, + config.repoId, + // TODO #22198 + pr.pullRequestId!, + pr.createdBy!.id!, + ); +} diff --git a/lib/modules/platform/gitlab/index.spec.ts b/lib/modules/platform/gitlab/index.spec.ts index 6eb62b886f7c40..e586ed9e49070d 100644 --- a/lib/modules/platform/gitlab/index.spec.ts +++ b/lib/modules/platform/gitlab/index.spec.ts @@ -128,9 +128,18 @@ describe('modules/platform/gitlab/index', () => { }); it(`should reuse existing gitAuthor`, async () => { - httpMock.scope(gitlabApiHost).get('/api/v4/version').reply(200, { - version: '13.3.6-ee', - }); + httpMock + .scope(gitlabApiHost) + .get('/api/v4/user') + .reply(200, { + email: 'different@email.com', + name: 'Different Name', + id: 1, + }) + .get('/api/v4/version') + .reply(200, { + version: '13.3.6-ee', + }); expect( await gitlab.initPlatform({ token: 'some-token', @@ -1900,6 +1909,7 @@ describe('modules/platform/gitlab/index', () => { .get('/api/v4/user') .reply(200, { email: 'a@b.com', + id: 1, name: 'Renovate Bot', }) .get('/api/v4/version') @@ -2778,6 +2788,10 @@ describe('modules/platform/gitlab/index', () => { target_branch: 'master', description: 'the-body', }) + .get('/api/v4/projects/undefined/merge_requests/12345/approvals') + .reply(200, { + approved_by: [], + }) .post('/api/v4/projects/undefined/merge_requests/12345/approve') .reply(200); expect( @@ -2811,6 +2825,8 @@ describe('modules/platform/gitlab/index', () => { target_branch: 'master', description: 'the-body', }) + .get('/api/v4/projects/undefined/merge_requests/12345/approvals') + .replyWithError('some error') .post('/api/v4/projects/undefined/merge_requests/12345/approve') .replyWithError('some error'); await expect( @@ -3139,6 +3155,104 @@ describe('modules/platform/gitlab/index', () => { description: 'body', state: 'opened', }) + .get('/api/v4/projects/undefined/merge_requests/1/approvals') + .reply(200, { + approved_by: [], + }) + .post('/api/v4/projects/undefined/merge_requests/1/approve') + .reply(200); + await expect( + gitlab.updatePr({ + number: 1, + prTitle: 'title', + prBody: 'body', + platformPrOptions: { + autoApprove: true, + }, + }), + ).toResolve(); + }); + + it('does not auto-approve if already approved', async () => { + await initPlatform('13.3.6-ee'); + httpMock + .scope(gitlabApiHost) + .get( + '/api/v4/projects/undefined/merge_requests?per_page=100&scope=created_by_me', + ) + .reply(200, [ + { + iid: 1, + source_branch: 'branch-a', + title: 'branch a pr', + description: 'a merge request', + state: 'opened', + }, + ]) + .put('/api/v4/projects/undefined/merge_requests/1') + .reply(200, { + iid: 1, + source_branch: 'branch-a', + title: 'title', + description: 'body', + state: 'opened', + }) + .get('/api/v4/projects/undefined/merge_requests/1/approvals') + .reply(200, { + approved_by: [ + { + user: { + id: 1, + }, + }, + ], + }); + await expect( + gitlab.updatePr({ + number: 1, + prTitle: 'title', + prBody: 'body', + platformPrOptions: { + autoApprove: true, + }, + }), + ).toResolve(); + }); + + it('auto-approves if approved by another user', async () => { + await initPlatform('13.3.6-ee'); + httpMock + .scope(gitlabApiHost) + .get( + '/api/v4/projects/undefined/merge_requests?per_page=100&scope=created_by_me', + ) + .reply(200, [ + { + iid: 1, + source_branch: 'branch-a', + title: 'branch a pr', + description: 'a merge request', + state: 'opened', + }, + ]) + .put('/api/v4/projects/undefined/merge_requests/1') + .reply(200, { + iid: 1, + source_branch: 'branch-a', + title: 'title', + description: 'body', + state: 'opened', + }) + .get('/api/v4/projects/undefined/merge_requests/1/approvals') + .reply(200, { + approved_by: [ + { + user: { + id: 2, + }, + }, + ], + }) .post('/api/v4/projects/undefined/merge_requests/1/approve') .reply(200); await expect( diff --git a/lib/modules/platform/gitlab/index.ts b/lib/modules/platform/gitlab/index.ts index 30965b96308505..f2a013dd7d585f 100644 --- a/lib/modules/platform/gitlab/index.ts +++ b/lib/modules/platform/gitlab/index.ts @@ -65,6 +65,7 @@ import { getMR, updateMR } from './merge-request'; import { LastPipelineId } from './schema'; import type { GitLabMergeRequest, + GitLabMergeRequestApprovals, GitlabComment, GitlabIssue, GitlabPr, @@ -94,6 +95,7 @@ const defaults = { export const id = 'gitlab'; let draftPrefix = DRAFT_PREFIX; +let renovateUserId: number; export async function initPlatform({ endpoint, @@ -114,19 +116,22 @@ export async function initPlatform({ }; let gitlabVersion: string; try { + const user = ( + await gitlabApi.getJson<{ + email: string; + name: string; + id: number; + commit_email?: string; + }>(`user`, { token }) + ).body; + + renovateUserId = user.id; if (!gitAuthor) { - const user = ( - await gitlabApi.getJson<{ - email: string; - name: string; - id: number; - commit_email?: string; - }>(`user`, { token }) - ).body; platformConfig.gitAuthor = `${user.name} <${ user.commit_email ?? user.email }>`; } + // istanbul ignore if: experimental feature if (process.env.RENOVATE_X_PLATFORM_VERSION) { gitlabVersion = process.env.RENOVATE_X_PLATFORM_VERSION; @@ -711,6 +716,26 @@ async function tryPrAutomerge( } async function approvePr(pr: number): Promise<void> { + try { + const { body: approvals } = + await gitlabApi.getJson<GitLabMergeRequestApprovals>( + `projects/${config.repository}/merge_requests/${pr}/approvals`, + ); + + const isApproved = approvals.approved_by?.some( + (approval) => approval.user?.id === renovateUserId, + ); + + if (isApproved) { + logger.debug('MR is already approved'); + return; + } + } catch (err) { + logger.warn( + { err }, + 'GitLab: Error checking if the merge request is approved', + ); + } try { await gitlabApi.postJson( `projects/${config.repository}/merge_requests/${pr}/approve`, diff --git a/lib/modules/platform/gitlab/types.ts b/lib/modules/platform/gitlab/types.ts index 244ec8366361e6..c37243b8a44bec 100644 --- a/lib/modules/platform/gitlab/types.ts +++ b/lib/modules/platform/gitlab/types.ts @@ -77,3 +77,7 @@ export interface GitlabUserStatus { emoji?: string; availability: 'not_set' | 'busy'; } + +export interface GitLabMergeRequestApprovals { + approved_by?: { user?: GitLabUser }[]; +} From dbdf41a9366a5d68478804fad40ff514ba1f2707 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sun, 1 Dec 2024 17:27:27 +0000 Subject: [PATCH 07/18] chore: add better jsdoc for approvePr --- lib/modules/platform/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index 67e95722462fa4..8336682efdcfbb 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -282,6 +282,10 @@ export interface Platform { maxBodyLength(): number; labelCharLimit?(): number; + /** + * For platforms that support `autoApprove`. It should handle when the PR + * is already be approved. + */ approvePr?(number: number): Promise<void>; } From bd05816dd1e873725d52f5ca312368df5238da4f Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sun, 1 Dec 2024 17:28:48 +0000 Subject: [PATCH 08/18] test(gerrit): fix lint in client.spec.ts --- lib/modules/platform/gerrit/client.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index 7ac3193a68262d..0c5ef3f265e204 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -4,8 +4,8 @@ import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages'; import { setBaseUrl } from '../../../util/http/gerrit'; import type { FindPRConfig } from '../types'; import { client } from './client'; -import type { - GerritAccountInfo, +import { + type GerritAccountInfo, type GerritChange, type GerritChangeMessageInfo, type GerritFindPRConfig, From ac30f2b204b2cb8acca3b0ebf534b6ee5726d543 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sun, 1 Dec 2024 17:32:33 +0000 Subject: [PATCH 09/18] test(automerge): add test for auto approve before merging --- lib/workers/repository/update/pr/automerge.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/workers/repository/update/pr/automerge.spec.ts b/lib/workers/repository/update/pr/automerge.spec.ts index 71d389ad126816..a8f68181dd57ab 100644 --- a/lib/workers/repository/update/pr/automerge.spec.ts +++ b/lib/workers/repository/update/pr/automerge.spec.ts @@ -124,6 +124,19 @@ describe('workers/repository/update/pr/automerge', () => { expect(platform.mergePr).toHaveBeenCalledTimes(0); }); + it('should attempt to auto-approve if supported', async () => { + config.autoApprove = true; + config.automerge = true; + config.pruneBranchAfterAutomerge = true; + platform.approvePr.mockResolvedValueOnce(); + platform.getBranchStatus.mockResolvedValueOnce('green'); + platform.mergePr.mockResolvedValueOnce(true); + const res = await prAutomerge.checkAutoMerge(pr, config); + expect(res).toEqual({ automerged: true, branchRemoved: true }); + expect(platform.approvePr).toHaveBeenCalledTimes(1); + expect(platform.mergePr).toHaveBeenCalledTimes(1); + }); + it('should not automerge if enabled and pr is unmergeable', async () => { config.automerge = true; scm.isBranchConflicted.mockResolvedValueOnce(true); From 7923d5b955e3d07c544b4736a83b06c1e0d27d12 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Mon, 2 Dec 2024 17:02:59 +0000 Subject: [PATCH 10/18] Revert "feat(gitlab,azure): try approving before auto-merge" This reverts commit c679c28d01c8513adee1feea63ef4b3a53491270. --- .../azure/__snapshots__/index.spec.ts.snap | 4 - lib/modules/platform/azure/index.spec.ts | 80 +----------- lib/modules/platform/azure/index.ts | 61 ++++----- lib/modules/platform/gitlab/index.spec.ts | 120 +----------------- lib/modules/platform/gitlab/index.ts | 41 ++---- lib/modules/platform/gitlab/types.ts | 4 - 6 files changed, 38 insertions(+), 272 deletions(-) diff --git a/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap b/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap index 4fed47efa74ab4..e782de620ace85 100644 --- a/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap +++ b/lib/modules/platform/azure/__snapshots__/index.spec.ts.snap @@ -217,10 +217,6 @@ exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOpti ] `; -exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOptions) should not re-approve if the PR is already approved 1`] = `undefined`; - -exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOptions) should re-approve if the PR was approved by another user 1`] = `undefined`; - exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOptions) should re-approve the PR 1`] = `undefined`; exports[`modules/platform/azure/index updatePr(prNo, title, body, platformPrOptions) should reopen the PR 1`] = ` diff --git a/lib/modules/platform/azure/index.spec.ts b/lib/modules/platform/azure/index.spec.ts index efc394cac94843..73308cdfbfa98e 100644 --- a/lib/modules/platform/azure/index.spec.ts +++ b/lib/modules/platform/azure/index.spec.ts @@ -1149,7 +1149,7 @@ describe('modules/platform/azure/index', () => { const updateFn = jest .fn(() => prUpdateResult) .mockName('createPullRequestReviewer'); - azureApi.gitApi.mockImplementation( + azureApi.gitApi.mockImplementationOnce( () => ({ createPullRequest: jest.fn(() => prResult), @@ -1293,82 +1293,7 @@ describe('modules/platform/azure/index', () => { isRequired: false, }; const updateFn = jest.fn(() => prUpdateResult); - azureApi.gitApi.mockImplementation( - () => - ({ - updatePullRequest: jest.fn(() => prResult), - createPullRequestReviewer: updateFn, - getPullRequestById: jest.fn(() => ({ - pullRequestId: prResult.pullRequestId, - createdBy: prResult.createdBy, - })), - }) as any, - ); - const pr = await azure.updatePr({ - number: prResult.pullRequestId, - prTitle: 'The Title', - prBody: 'Hello world', - platformPrOptions: { autoApprove: true }, - }); - expect(updateFn).toHaveBeenCalled(); - expect(pr).toMatchSnapshot(); - }); - - it('should not re-approve if the PR is already approved', async () => { - await initRepo({ repository: 'some/repo' }); - const prResult = { - pullRequestId: 456, - createdBy: { - id: 123, - url: 'user-url', - }, - }; - const prUpdateResult = { - reviewerUrl: prResult.createdBy.url, - vote: AzurePrVote.Approved, - isFlagged: false, - isRequired: false, - }; - const updateFn = jest.fn(() => prUpdateResult); - azureApi.gitApi.mockImplementation( - () => - ({ - updatePullRequest: jest.fn(() => prResult), - createPullRequestReviewer: updateFn, - getPullRequestById: jest.fn(() => ({ - pullRequestId: prResult.pullRequestId, - createdBy: prResult.createdBy, - reviewers: [{ vote: AzurePrVote.Approved, id: 123 }], - })), - }) as any, - ); - const pr = await azure.updatePr({ - number: prResult.pullRequestId, - prTitle: 'The Title', - prBody: 'Hello world', - platformPrOptions: { autoApprove: true }, - }); - expect(updateFn).not.toHaveBeenCalled(); - expect(pr).toMatchSnapshot(); - }); - - it('should re-approve if the PR was approved by another user', async () => { - await initRepo({ repository: 'some/repo' }); - const prResult = { - pullRequestId: 456, - createdBy: { - id: 123, - url: 'user-url', - }, - }; - const prUpdateResult = { - reviewerUrl: prResult.createdBy.url, - vote: AzurePrVote.Approved, - isFlagged: false, - isRequired: false, - }; - const updateFn = jest.fn(() => prUpdateResult); - azureApi.gitApi.mockImplementation( + azureApi.gitApi.mockImplementationOnce( () => ({ updatePullRequest: jest.fn(() => prResult), @@ -1376,7 +1301,6 @@ describe('modules/platform/azure/index', () => { getPullRequestById: jest.fn(() => ({ pullRequestId: prResult.pullRequestId, createdBy: prResult.createdBy, - reviewers: [{ vote: AzurePrVote.Approved, id: 321 }], })), }) as any, ); diff --git a/lib/modules/platform/azure/index.ts b/lib/modules/platform/azure/index.ts index 039e7e18f7badc..21bd44eaa3f58c 100644 --- a/lib/modules/platform/azure/index.ts +++ b/lib/modules/platform/azure/index.ts @@ -513,7 +513,18 @@ export async function createPr({ ); } if (platformPrOptions?.autoApprove) { - await approvePr(pr); + await azureApiGit.createPullRequestReviewer( + { + reviewerUrl: pr.createdBy!.url, + vote: AzurePrVote.Approved, + isFlagged: false, + isRequired: false, + }, + config.repoId, + // TODO #22198 + pr.pullRequestId!, + pr.createdBy!.id!, + ); } await Promise.all( labels!.map((label) => @@ -570,7 +581,19 @@ export async function updatePr({ objToUpdate.status = PullRequestStatus.Abandoned; } if (platformPrOptions?.autoApprove) { - await approvePr(prNo); + const pr = await azureApiGit.getPullRequestById(prNo, config.project); + await azureApiGit.createPullRequestReviewer( + { + reviewerUrl: pr.createdBy!.url, + vote: AzurePrVote.Approved, + isFlagged: false, + isRequired: false, + }, + config.repoId, + // TODO #22198 + pr.pullRequestId!, + pr.createdBy!.id!, + ); } const updatedPr = await azureApiGit.updatePullRequest( @@ -992,37 +1015,3 @@ export async function deleteLabel( const azureApiGit = await azureApi.gitApi(); await azureApiGit.deletePullRequestLabels(config.repoId, prNumber, label); } - -export async function approvePr( - prNumberOrPr: number | GitPullRequest, -): Promise<void> { - const azureApiGit = await azureApi.gitApi(); - const pr = - typeof prNumberOrPr === 'number' - ? await azureApiGit.getPullRequestById(prNumberOrPr, config.project) - : prNumberOrPr; - - const isApproved = pr.reviewers?.some( - (reviewer) => - reviewer.vote === AzurePrVote.Approved && - reviewer.id === pr.createdBy?.id, - ); - - if (isApproved) { - logger.debug('PR is already approved'); - return; - } - - await azureApiGit.createPullRequestReviewer( - { - reviewerUrl: pr.createdBy!.url, - vote: AzurePrVote.Approved, - isFlagged: false, - isRequired: false, - }, - config.repoId, - // TODO #22198 - pr.pullRequestId!, - pr.createdBy!.id!, - ); -} diff --git a/lib/modules/platform/gitlab/index.spec.ts b/lib/modules/platform/gitlab/index.spec.ts index e586ed9e49070d..6eb62b886f7c40 100644 --- a/lib/modules/platform/gitlab/index.spec.ts +++ b/lib/modules/platform/gitlab/index.spec.ts @@ -128,18 +128,9 @@ describe('modules/platform/gitlab/index', () => { }); it(`should reuse existing gitAuthor`, async () => { - httpMock - .scope(gitlabApiHost) - .get('/api/v4/user') - .reply(200, { - email: 'different@email.com', - name: 'Different Name', - id: 1, - }) - .get('/api/v4/version') - .reply(200, { - version: '13.3.6-ee', - }); + httpMock.scope(gitlabApiHost).get('/api/v4/version').reply(200, { + version: '13.3.6-ee', + }); expect( await gitlab.initPlatform({ token: 'some-token', @@ -1909,7 +1900,6 @@ describe('modules/platform/gitlab/index', () => { .get('/api/v4/user') .reply(200, { email: 'a@b.com', - id: 1, name: 'Renovate Bot', }) .get('/api/v4/version') @@ -2788,10 +2778,6 @@ describe('modules/platform/gitlab/index', () => { target_branch: 'master', description: 'the-body', }) - .get('/api/v4/projects/undefined/merge_requests/12345/approvals') - .reply(200, { - approved_by: [], - }) .post('/api/v4/projects/undefined/merge_requests/12345/approve') .reply(200); expect( @@ -2825,8 +2811,6 @@ describe('modules/platform/gitlab/index', () => { target_branch: 'master', description: 'the-body', }) - .get('/api/v4/projects/undefined/merge_requests/12345/approvals') - .replyWithError('some error') .post('/api/v4/projects/undefined/merge_requests/12345/approve') .replyWithError('some error'); await expect( @@ -3155,104 +3139,6 @@ describe('modules/platform/gitlab/index', () => { description: 'body', state: 'opened', }) - .get('/api/v4/projects/undefined/merge_requests/1/approvals') - .reply(200, { - approved_by: [], - }) - .post('/api/v4/projects/undefined/merge_requests/1/approve') - .reply(200); - await expect( - gitlab.updatePr({ - number: 1, - prTitle: 'title', - prBody: 'body', - platformPrOptions: { - autoApprove: true, - }, - }), - ).toResolve(); - }); - - it('does not auto-approve if already approved', async () => { - await initPlatform('13.3.6-ee'); - httpMock - .scope(gitlabApiHost) - .get( - '/api/v4/projects/undefined/merge_requests?per_page=100&scope=created_by_me', - ) - .reply(200, [ - { - iid: 1, - source_branch: 'branch-a', - title: 'branch a pr', - description: 'a merge request', - state: 'opened', - }, - ]) - .put('/api/v4/projects/undefined/merge_requests/1') - .reply(200, { - iid: 1, - source_branch: 'branch-a', - title: 'title', - description: 'body', - state: 'opened', - }) - .get('/api/v4/projects/undefined/merge_requests/1/approvals') - .reply(200, { - approved_by: [ - { - user: { - id: 1, - }, - }, - ], - }); - await expect( - gitlab.updatePr({ - number: 1, - prTitle: 'title', - prBody: 'body', - platformPrOptions: { - autoApprove: true, - }, - }), - ).toResolve(); - }); - - it('auto-approves if approved by another user', async () => { - await initPlatform('13.3.6-ee'); - httpMock - .scope(gitlabApiHost) - .get( - '/api/v4/projects/undefined/merge_requests?per_page=100&scope=created_by_me', - ) - .reply(200, [ - { - iid: 1, - source_branch: 'branch-a', - title: 'branch a pr', - description: 'a merge request', - state: 'opened', - }, - ]) - .put('/api/v4/projects/undefined/merge_requests/1') - .reply(200, { - iid: 1, - source_branch: 'branch-a', - title: 'title', - description: 'body', - state: 'opened', - }) - .get('/api/v4/projects/undefined/merge_requests/1/approvals') - .reply(200, { - approved_by: [ - { - user: { - id: 2, - }, - }, - ], - }) .post('/api/v4/projects/undefined/merge_requests/1/approve') .reply(200); await expect( diff --git a/lib/modules/platform/gitlab/index.ts b/lib/modules/platform/gitlab/index.ts index f2a013dd7d585f..30965b96308505 100644 --- a/lib/modules/platform/gitlab/index.ts +++ b/lib/modules/platform/gitlab/index.ts @@ -65,7 +65,6 @@ import { getMR, updateMR } from './merge-request'; import { LastPipelineId } from './schema'; import type { GitLabMergeRequest, - GitLabMergeRequestApprovals, GitlabComment, GitlabIssue, GitlabPr, @@ -95,7 +94,6 @@ const defaults = { export const id = 'gitlab'; let draftPrefix = DRAFT_PREFIX; -let renovateUserId: number; export async function initPlatform({ endpoint, @@ -116,22 +114,19 @@ export async function initPlatform({ }; let gitlabVersion: string; try { - const user = ( - await gitlabApi.getJson<{ - email: string; - name: string; - id: number; - commit_email?: string; - }>(`user`, { token }) - ).body; - - renovateUserId = user.id; if (!gitAuthor) { + const user = ( + await gitlabApi.getJson<{ + email: string; + name: string; + id: number; + commit_email?: string; + }>(`user`, { token }) + ).body; platformConfig.gitAuthor = `${user.name} <${ user.commit_email ?? user.email }>`; } - // istanbul ignore if: experimental feature if (process.env.RENOVATE_X_PLATFORM_VERSION) { gitlabVersion = process.env.RENOVATE_X_PLATFORM_VERSION; @@ -716,26 +711,6 @@ async function tryPrAutomerge( } async function approvePr(pr: number): Promise<void> { - try { - const { body: approvals } = - await gitlabApi.getJson<GitLabMergeRequestApprovals>( - `projects/${config.repository}/merge_requests/${pr}/approvals`, - ); - - const isApproved = approvals.approved_by?.some( - (approval) => approval.user?.id === renovateUserId, - ); - - if (isApproved) { - logger.debug('MR is already approved'); - return; - } - } catch (err) { - logger.warn( - { err }, - 'GitLab: Error checking if the merge request is approved', - ); - } try { await gitlabApi.postJson( `projects/${config.repository}/merge_requests/${pr}/approve`, diff --git a/lib/modules/platform/gitlab/types.ts b/lib/modules/platform/gitlab/types.ts index c37243b8a44bec..244ec8366361e6 100644 --- a/lib/modules/platform/gitlab/types.ts +++ b/lib/modules/platform/gitlab/types.ts @@ -77,7 +77,3 @@ export interface GitlabUserStatus { emoji?: string; availability: 'not_set' | 'busy'; } - -export interface GitLabMergeRequestApprovals { - approved_by?: { user?: GitLabUser }[]; -} From c4c81e30fc32f68f62dd1e9bc6a43d20e4d8cfbe Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Mon, 2 Dec 2024 17:18:17 +0000 Subject: [PATCH 11/18] chore: rename approvePr to approvePrForAutomerge --- lib/modules/platform/gerrit/index.ts | 13 ++++++++++--- lib/modules/platform/types.ts | 6 +++--- lib/workers/repository/update/pr/automerge.spec.ts | 4 ++-- lib/workers/repository/update/pr/automerge.ts | 6 +++--- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index a3246f5da38aa0..206283afb99595 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -161,7 +161,7 @@ export async function updatePr(prConfig: UpdatePrConfig): Promise<void> { ); } if (prConfig.platformPrOptions?.autoApprove) { - await approvePr(prConfig.number); + await client.approveChange(prConfig.number); } if (prConfig.state && prConfig.state === 'closed') { await client.abandonChange(prConfig.number); @@ -196,7 +196,7 @@ export async function createPr(prConfig: CreatePRConfig): Promise<Pr | null> { TAG_PULL_REQUEST_BODY, ); if (prConfig.platformPrOptions?.autoApprove) { - await approvePr(pr._number); + await client.approveChange(pr._number); } return getPr(pr._number); } @@ -443,6 +443,13 @@ export function getIssueList(): Promise<Issue[]> { return Promise.resolve([]); } -export async function approvePr(number: number): Promise<void> { +/** + * The Code-Review +2 posted when the change was created or updated in Gerrit + * may have been downgraded by a CI check utilizing the same account as + * Renovate (e.g. SonarQube which posts Code-Review +1). This function will + * post a +2 again on the change, if needed, before Renovate attempt to + * automerge it. + */ +export async function approvePrForAutomerge(number: number): Promise<void> { await client.approveChange(number); } diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index 8336682efdcfbb..cfd3126068e2d4 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -283,10 +283,10 @@ export interface Platform { maxBodyLength(): number; labelCharLimit?(): number; /** - * For platforms that support `autoApprove`. It should handle when the PR - * is already be approved. + * Platforms that support `autoApprove` can implement this function. It will be + * invoked during automerge before the PR branch status is checked. */ - approvePr?(number: number): Promise<void>; + approvePrForAutomerge?(number: number): Promise<void>; } export interface PlatformScm { diff --git a/lib/workers/repository/update/pr/automerge.spec.ts b/lib/workers/repository/update/pr/automerge.spec.ts index a8f68181dd57ab..1d5e0a70127b13 100644 --- a/lib/workers/repository/update/pr/automerge.spec.ts +++ b/lib/workers/repository/update/pr/automerge.spec.ts @@ -128,12 +128,12 @@ describe('workers/repository/update/pr/automerge', () => { config.autoApprove = true; config.automerge = true; config.pruneBranchAfterAutomerge = true; - platform.approvePr.mockResolvedValueOnce(); + platform.approvePrForAutomerge.mockResolvedValueOnce(); platform.getBranchStatus.mockResolvedValueOnce('green'); platform.mergePr.mockResolvedValueOnce(true); const res = await prAutomerge.checkAutoMerge(pr, config); expect(res).toEqual({ automerged: true, branchRemoved: true }); - expect(platform.approvePr).toHaveBeenCalledTimes(1); + expect(platform.approvePrForAutomerge).toHaveBeenCalledTimes(1); expect(platform.mergePr).toHaveBeenCalledTimes(1); }); diff --git a/lib/workers/repository/update/pr/automerge.ts b/lib/workers/repository/update/pr/automerge.ts index 4bbe2aaab1d53e..9803adc7f8c242 100644 --- a/lib/workers/repository/update/pr/automerge.ts +++ b/lib/workers/repository/update/pr/automerge.ts @@ -71,9 +71,9 @@ export async function checkAutoMerge( } // Usually the PR will already be approved, this is a last resort in case the // approval was lost for some reason - if (config.autoApprove && platform.approvePr) { - logger.debug('Auto-approving PR if needed before automerge'); - await platform.approvePr(pr.number); + if (config.autoApprove && platform.approvePrForAutomerge) { + logger.debug('Auto-approving PR for automerge'); + await platform.approvePrForAutomerge(pr.number); } const branchStatus = await resolveBranchStatus( branchName, From 4dfc8177933d80e08152e0dcb88690159495d33b Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Mon, 2 Dec 2024 17:20:47 +0000 Subject: [PATCH 12/18] Revert "chore(gerrit): still approve if change was approved another user" This reverts commit 5eb284603f3afdcb0223d036c9d029482071e46b. --- lib/modules/platform/gerrit/client.spec.ts | 34 +--------------------- lib/modules/platform/gerrit/client.ts | 13 ++------- lib/modules/platform/gerrit/types.ts | 1 - 3 files changed, 4 insertions(+), 44 deletions(-) diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index 0c5ef3f265e204..a74830ea610932 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -5,7 +5,6 @@ import { setBaseUrl } from '../../../util/http/gerrit'; import type { FindPRConfig } from '../types'; import { client } from './client'; import { - type GerritAccountInfo, type GerritChange, type GerritChangeMessageInfo, type GerritFindPRConfig, @@ -400,14 +399,12 @@ describe('modules/platform/gerrit/client', () => { describe('approveChange()', () => { it('already approved - do nothing', async () => { - const owner = partial<GerritAccountInfo>({ username: 'user' }); const change = partial<GerritChange>({ labels: { 'Code-Review': { - all: [{ value: 2, username: owner.username }], + all: [{ value: 2 }], }, }, - owner, }); httpMock .scope(gerritEndpointUrl) @@ -483,35 +480,6 @@ describe('modules/platform/gerrit/client', () => { await expect(client.approveChange(123456)).toResolve(); expect(approveMock.isDone()).toBeTrue(); }); - - it('already approved by another user - approve again', async () => { - const owner = partial<GerritAccountInfo>({ username: 'user' }); - const change = partial<GerritChange>({ - labels: { - 'Code-Review': { - all: [{ value: 2, username: 'other user' }], - }, - }, - owner, - }); - httpMock - .scope(gerritEndpointUrl) - .get( - (url) => - url.includes('/a/changes/123456?o=') && - url.includes('o=DETAILED_LABELS'), - ) - .reply(200, gerritRestResponse(change), jsonResultHeader); - const approveMock = httpMock - .scope(gerritEndpointUrl) - .post('/a/changes/123456/revisions/current/review', { - labels: { 'Code-Review': +2 }, - notify: 'NONE', - }) - .reply(200, gerritRestResponse(''), jsonResultHeader); - await expect(client.approveChange(123456)).toResolve(); - expect(approveMock.isDone()).toBeTrue(); - }); }); }); diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index 74221a497da71c..83b82454ad141c 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -193,11 +193,9 @@ class GerritClient { async approveChange(changeId: number): Promise<void> { const isApproved = await this.checkIfApproved(changeId); - if (isApproved) { - logger.debug(`Change is already approved`); - return; + if (!isApproved) { + await this.setLabel(changeId, 'Code-Review', +2); } - await this.setLabel(changeId, 'Code-Review', +2); } async checkIfApproved(changeId: number): Promise<boolean> { @@ -205,12 +203,7 @@ class GerritClient { const reviewLabel = change?.labels?.['Code-Review']; return ( reviewLabel === undefined || - Boolean( - reviewLabel.all?.some( - (label) => - label.value === 2 && label.username === change.owner.username, - ), - ) + Boolean(reviewLabel.all?.some((label) => label.value === 2)) ); } diff --git a/lib/modules/platform/gerrit/types.ts b/lib/modules/platform/gerrit/types.ts index 9cf35154a59468..b65038100b3182 100644 --- a/lib/modules/platform/gerrit/types.ts +++ b/lib/modules/platform/gerrit/types.ts @@ -52,7 +52,6 @@ export interface GerritChange { */ revisions: Record<string, GerritRevisionInfo>; problems: unknown[]; - owner: GerritAccountInfo; } export interface GerritCommitInfo { From bb317ce921bb4d7d27c56683908d8aa8f82933a4 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Mon, 2 Dec 2024 17:30:54 +0000 Subject: [PATCH 13/18] chore(gerrit): improve approve method to avoid some API calls --- lib/modules/platform/gerrit/index.spec.ts | 44 +---------------------- lib/modules/platform/gerrit/index.ts | 10 ++---- lib/modules/platform/gerrit/scm.spec.ts | 3 +- lib/modules/platform/gerrit/scm.ts | 10 +++--- 4 files changed, 9 insertions(+), 58 deletions(-) diff --git a/lib/modules/platform/gerrit/index.spec.ts b/lib/modules/platform/gerrit/index.spec.ts index 3b9c456f1852e4..692dad06d982fd 100644 --- a/lib/modules/platform/gerrit/index.spec.ts +++ b/lib/modules/platform/gerrit/index.spec.ts @@ -187,28 +187,6 @@ describe('modules/platform/gerrit/index', () => { gerrit.writeToConfig({ labels: {} }); }); - it('updatePr() - auto approve enabled', async () => { - const change = partial<GerritChange>({ - current_revision: 'some-revision', - revisions: { - 'some-revision': partial<GerritRevisionInfo>({ - commit: { - message: 'some message', - }, - }), - }, - }); - clientMock.getChange.mockResolvedValueOnce(change); - await gerrit.updatePr({ - number: 123456, - prTitle: 'subject', - platformPrOptions: { - autoApprove: true, - }, - }); - expect(clientMock.approveChange).toHaveBeenCalledWith(123456); - }); - it('updatePr() - closed => abandon the change', async () => { const change = partial<GerritChange>({}); clientMock.getChange.mockResolvedValueOnce(change); @@ -309,7 +287,7 @@ describe('modules/platform/gerrit/index', () => { ]); }); - it('createPr() - update body WITHOUT approve', async () => { + it('createPr() - update body', async () => { const pr = await gerrit.createPr({ sourceBranch: 'source', targetBranch: 'target', @@ -325,26 +303,6 @@ describe('modules/platform/gerrit/index', () => { 'body', TAG_PULL_REQUEST_BODY, ); - expect(clientMock.approveChange).not.toHaveBeenCalled(); - }); - - it('createPr() - update body and approve', async () => { - const pr = await gerrit.createPr({ - sourceBranch: 'source', - targetBranch: 'target', - prTitle: change.subject, - prBody: 'body', - platformPrOptions: { - autoApprove: true, - }, - }); - expect(pr).toHaveProperty('number', 123456); - expect(clientMock.addMessageIfNotAlreadyExists).toHaveBeenCalledWith( - 123456, - 'body', - TAG_PULL_REQUEST_BODY, - ); - expect(clientMock.approveChange).toHaveBeenCalledWith(123456); }); }); diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index 206283afb99595..98afc1a271bdd6 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -160,9 +160,6 @@ export async function updatePr(prConfig: UpdatePrConfig): Promise<void> { TAG_PULL_REQUEST_BODY, ); } - if (prConfig.platformPrOptions?.autoApprove) { - await client.approveChange(prConfig.number); - } if (prConfig.state && prConfig.state === 'closed') { await client.abandonChange(prConfig.number); } @@ -195,9 +192,6 @@ export async function createPr(prConfig: CreatePRConfig): Promise<Pr | null> { prConfig.prBody, TAG_PULL_REQUEST_BODY, ); - if (prConfig.platformPrOptions?.autoApprove) { - await client.approveChange(pr._number); - } return getPr(pr._number); } @@ -444,10 +438,10 @@ export function getIssueList(): Promise<Issue[]> { } /** - * The Code-Review +2 posted when the change was created or updated in Gerrit + * The Code-Review +2 vote of when the change was created or updated in Gerrit * may have been downgraded by a CI check utilizing the same account as * Renovate (e.g. SonarQube which posts Code-Review +1). This function will - * post a +2 again on the change, if needed, before Renovate attempt to + * vote with +2 again on the change, if needed, before Renovate attempt to * automerge it. */ export async function approvePrForAutomerge(number: number): Promise<void> { diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 77c4797bda18c6..1d50b6e43ef069 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -403,9 +403,8 @@ describe('modules/platform/gerrit/scm', () => { expect(git.pushCommit).toHaveBeenCalledWith({ files: [], sourceRef: 'renovate/dependency-1.x', - targetRef: 'refs/for/main%notify=NONE', + targetRef: 'refs/for/main%notify=NONE,l=Code-Review+2', }); - expect(clientMock.approveChange).toHaveBeenCalledWith(123456); }); }); }); diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index 01e79df330dd69..a81bd5d68de1ea 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -129,16 +129,16 @@ export class GerritScm extends DefaultGitScm { hasChanges = await git.hasDiff('HEAD', 'FETCH_HEAD'); //avoid empty patchsets } if (hasChanges || commit.force) { + const pushOptions = ['notify=NONE']; + if (commit.autoApprove) { + pushOptions.push('l=Code-Review+2'); + } const pushResult = await git.pushCommit({ sourceRef: commit.branchName, - targetRef: `refs/for/${commit.baseBranch!}%notify=NONE`, + targetRef: `refs/for/${commit.baseBranch!}%${pushOptions.join(',')}`, files: commit.files, }); if (pushResult) { - // New patchsets dismisses the approval, so we need to approve it again - if (existingChange && commit.autoApprove) { - await client.approveChange(existingChange._number); - } return commitSha; } } From 53743b27bf837fe8109639872944a5b0d5906d22 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Mon, 2 Dec 2024 17:38:39 +0000 Subject: [PATCH 14/18] chore: fix type import --- lib/modules/platform/gerrit/client.spec.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index a74830ea610932..a451bd020d9d46 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -4,11 +4,11 @@ import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages'; import { setBaseUrl } from '../../../util/http/gerrit'; import type { FindPRConfig } from '../types'; import { client } from './client'; -import { - type GerritChange, - type GerritChangeMessageInfo, - type GerritFindPRConfig, - type GerritMergeableInfo, +import type { + GerritChange, + GerritChangeMessageInfo, + GerritFindPRConfig, + GerritMergeableInfo, } from './types'; const gerritEndpointUrl = 'https://dev.gerrit.com/renovate/'; From e0b6f8d5ed95712431d5a5f833369528df7f9d1d Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Mon, 2 Dec 2024 17:41:35 +0000 Subject: [PATCH 15/18] chore: remove spurious comment --- lib/workers/repository/update/pr/automerge.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/workers/repository/update/pr/automerge.ts b/lib/workers/repository/update/pr/automerge.ts index 9803adc7f8c242..255f185cd6a23e 100644 --- a/lib/workers/repository/update/pr/automerge.ts +++ b/lib/workers/repository/update/pr/automerge.ts @@ -69,8 +69,6 @@ export async function checkAutoMerge( prAutomergeBlockReason: 'PlatformNotReady', }; } - // Usually the PR will already be approved, this is a last resort in case the - // approval was lost for some reason if (config.autoApprove && platform.approvePrForAutomerge) { logger.debug('Auto-approving PR for automerge'); await platform.approvePrForAutomerge(pr.number); From b93ee7a628c80f4eb8cfe339834c313b7fc75c2e Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sun, 29 Dec 2024 15:52:37 +0000 Subject: [PATCH 16/18] Address review comments --- lib/modules/platform/gerrit/client.ts | 6 ++++-- lib/workers/repository/update/pr/automerge.spec.ts | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index 83b82454ad141c..e255d44a6a9bb5 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -2,6 +2,7 @@ import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages'; import { logger } from '../../../logger'; import { GerritHttp } from '../../../util/http/gerrit'; import { regEx } from '../../../util/regex'; +import { getQueryString } from '../../../util/url'; import type { GerritAccountInfo, GerritBranchInfo, @@ -77,8 +78,9 @@ class GerritClient { extraOptions?: string[], ): Promise<GerritChange> { const options = [...this.requestDetails, ...(extraOptions ?? [])]; + const queryString = getQueryString({ o: options }); const changes = await this.gerritHttp.getJson<GerritChange>( - `a/changes/${changeNumber}?` + options.map((det) => `o=${det}`).join('&'), + `a/changes/${changeNumber}?${queryString}`, ); return changes.body; } @@ -203,7 +205,7 @@ class GerritClient { const reviewLabel = change?.labels?.['Code-Review']; return ( reviewLabel === undefined || - Boolean(reviewLabel.all?.some((label) => label.value === 2)) + reviewLabel.all?.some((label) => label.value === 2) === true ); } diff --git a/lib/workers/repository/update/pr/automerge.spec.ts b/lib/workers/repository/update/pr/automerge.spec.ts index 1d5e0a70127b13..b89690d773784d 100644 --- a/lib/workers/repository/update/pr/automerge.spec.ts +++ b/lib/workers/repository/update/pr/automerge.spec.ts @@ -128,7 +128,6 @@ describe('workers/repository/update/pr/automerge', () => { config.autoApprove = true; config.automerge = true; config.pruneBranchAfterAutomerge = true; - platform.approvePrForAutomerge.mockResolvedValueOnce(); platform.getBranchStatus.mockResolvedValueOnce('green'); platform.mergePr.mockResolvedValueOnce(true); const res = await prAutomerge.checkAutoMerge(pr, config); From add6ca57200bdd28586078ac478c79946e2949f1 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sun, 29 Dec 2024 16:04:44 +0000 Subject: [PATCH 17/18] Add tests for approvePrForAutomerge --- lib/modules/platform/gerrit/index.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/modules/platform/gerrit/index.spec.ts b/lib/modules/platform/gerrit/index.spec.ts index 692dad06d982fd..17fa9e54deb747 100644 --- a/lib/modules/platform/gerrit/index.spec.ts +++ b/lib/modules/platform/gerrit/index.spec.ts @@ -708,6 +708,13 @@ describe('modules/platform/gerrit/index', () => { //TODO: add some tests for Gerrit-specific replacements.. }); + describe('approvePrForAutomerge()', () => { + it('approvePrForAutomerge() - calls approveChange', async () => { + await expect(gerrit.approvePrForAutomerge(123456)).toResolve(); + expect(clientMock.approveChange).toHaveBeenCalledWith(123456); + }); + }); + describe('currently unused/not-implemented functions', () => { it('deleteLabel()', async () => { await expect( From a6d9d910e6862ab2f8827e0d72351011e787ad89 Mon Sep 17 00:00:00 2001 From: Felipe Santos <felipecassiors@gmail.com> Date: Sun, 29 Dec 2024 16:10:26 +0000 Subject: [PATCH 18/18] refactor(gerrit): remove deprecated source branch as hashtags support --- lib/modules/platform/gerrit/client.spec.ts | 1 - lib/modules/platform/gerrit/client.ts | 11 +----- lib/modules/platform/gerrit/readme.md | 6 ---- lib/modules/platform/gerrit/types.ts | 4 --- lib/modules/platform/gerrit/utils.spec.ts | 41 ---------------------- lib/modules/platform/gerrit/utils.ts | 7 ---- 6 files changed, 1 insertion(+), 69 deletions(-) diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index a451bd020d9d46..8411e5f4405239 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -102,7 +102,6 @@ describe('modules/platform/gerrit/client', () => { 'footer:Renovate-Branch=dependency-xyz', { branchName: 'dependency-xyz' }, ], - ['hashtag:sourceBranch-dependency-xyz', { branchName: 'dependency-xyz' }], // for backwards compatibility ['label:Code-Review=-2', { branchName: 'dependency-xyz', label: '-2' }], [ 'branch:otherTarget', diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index e255d44a6a9bb5..07460066d132e2 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -221,16 +221,7 @@ class GerritClient { const filterState = mapPrStateToGerritFilter(searchConfig.state); const filters = ['owner:self', 'project:' + repository, filterState]; if (searchConfig.branchName) { - filters.push( - ...[ - '(', - `footer:Renovate-Branch=${searchConfig.branchName}`, - // for backwards compatibility - 'OR', - `hashtag:sourceBranch-${searchConfig.branchName}`, - ')', - ], - ); + filters.push(`footer:Renovate-Branch=${searchConfig.branchName}`); } if (searchConfig.targetBranch) { filters.push(`branch:${searchConfig.targetBranch}`); diff --git a/lib/modules/platform/gerrit/readme.md b/lib/modules/platform/gerrit/readme.md index cdf994cf29fd4a..bb649c28fbbcdb 100644 --- a/lib/modules/platform/gerrit/readme.md +++ b/lib/modules/platform/gerrit/readme.md @@ -8,12 +8,6 @@ Support for Gerrit is currently _experimental_, meaning that it _might_ still ha Renovate stores its metadata in the _commit message footer_. -Previously Renovate stored metadata in Gerrit's _hashtags_. -To keep backwards compatibility, Renovate still reads metadata from hashtags. -But Renovate _always_ puts its metadata in the _commit message footer_! -When the Renovate maintainers mark Gerrit support as stable, the maintainers will remove the "read metadata from hashtags" feature. -This means changes without metadata in the commit message footer will be "forgotten" by Renovate. - ## Authentication <figure markdown> diff --git a/lib/modules/platform/gerrit/types.ts b/lib/modules/platform/gerrit/types.ts index b65038100b3182..07087105d4f6ca 100644 --- a/lib/modules/platform/gerrit/types.ts +++ b/lib/modules/platform/gerrit/types.ts @@ -34,10 +34,6 @@ export type GerritReviewersType = 'REVIEWER' | 'CC' | 'REMOVED'; export interface GerritChange { branch: string; - /** - * for backwards compatibility - */ - hashtags?: string[]; change_id: string; subject: string; status: GerritChangeStatus; diff --git a/lib/modules/platform/gerrit/utils.spec.ts b/lib/modules/platform/gerrit/utils.spec.ts index b609fdf7a88c0e..9505ed65624f11 100644 --- a/lib/modules/platform/gerrit/utils.spec.ts +++ b/lib/modules/platform/gerrit/utils.spec.ts @@ -186,47 +186,6 @@ describe('modules/platform/gerrit/utils', () => { }); expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x'); }); - - // for backwards compatibility - it('no commit message but with hashtags', () => { - const change = partial<GerritChange>({ - hashtags: ['sourceBranch-renovate/dependency-1.x'], - }); - expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x'); - }); - - // for backwards compatibility - it('commit message with no footer but with hashtags', () => { - const change = partial<GerritChange>({ - hashtags: ['sourceBranch-renovate/dependency-1.x'], - current_revision: 'abc', - revisions: { - abc: partial<GerritRevisionInfo>({ - commit: { - message: 'some message...', - }, - }), - }, - }); - expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x'); - }); - - // for backwards compatibility - it('prefers the footer over the hashtags', () => { - const change = partial<GerritChange>({ - hashtags: ['sourceBranch-renovate/dependency-1.x'], - current_revision: 'abc', - revisions: { - abc: partial<GerritRevisionInfo>({ - commit: { - message: - 'Some change\n\nRenovate-Branch: renovate/dependency-2.x\nChange-Id: ...', - }, - }), - }, - }); - expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-2.x'); - }); }); describe('findPullRequestBody()', () => { diff --git a/lib/modules/platform/gerrit/utils.ts b/lib/modules/platform/gerrit/utils.ts index 3eb28e4b8ece47..a96debaf7c9b82 100644 --- a/lib/modules/platform/gerrit/utils.ts +++ b/lib/modules/platform/gerrit/utils.ts @@ -101,13 +101,6 @@ export function extractSourceBranch(change: GerritChange): string | undefined { } } - // for backwards compatibility - if (!sourceBranch) { - sourceBranch = change.hashtags - ?.find((tag) => tag.startsWith('sourceBranch-')) - ?.replace('sourceBranch-', ''); - } - return sourceBranch ?? undefined; }