From 69d72c260aaef191a568dfa3c8ee3377d0540fa6 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Thu, 13 Nov 2025 23:13:09 -0300 Subject: [PATCH 01/35] fix(gerrit): defer change creation to `createPr()` Signed-off-by: Felipe Santos --- lib/modules/platform/gerrit/index.spec.ts | 127 +++++++++++++++------- lib/modules/platform/gerrit/index.ts | 38 +++++-- lib/modules/platform/gerrit/scm.spec.ts | 110 ++----------------- lib/modules/platform/gerrit/scm.ts | 43 +++++--- 4 files changed, 152 insertions(+), 166 deletions(-) diff --git a/lib/modules/platform/gerrit/index.spec.ts b/lib/modules/platform/gerrit/index.spec.ts index c7a472864ce..cf3aed37025 100644 --- a/lib/modules/platform/gerrit/index.spec.ts +++ b/lib/modules/platform/gerrit/index.spec.ts @@ -1,5 +1,4 @@ import { codeBlock } from 'common-tags'; -import { DateTime } from 'luxon'; import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages'; import type { BranchStatus } from '../../../types'; import { repoFingerprint } from '../util'; @@ -39,15 +38,6 @@ vi.mock('./client'); const clientMock = vi.mocked(_client); describe('modules/platform/gerrit/index', () => { - const t0 = DateTime.fromISO('2025-04-14T16:33:37.000000000', { - zone: 'utc', - }) as DateTime; - - beforeAll(() => { - vi.useFakeTimers(); - vi.setSystemTime(t0.toMillis()); - }); - beforeEach(async () => { hostRules.find.mockReturnValue({ username: 'user', @@ -257,24 +247,42 @@ describe('modules/platform/gerrit/index', () => { }); describe('createPr()', () => { - it('createPr() - no existing found => rejects', async () => { - clientMock.findChanges.mockResolvedValueOnce([]); - await expect( - gerrit.createPr({ - sourceBranch: 'source', - targetBranch: 'target', - prTitle: 'title', - prBody: 'body', - }), - ).rejects.toThrow( - `the change should be created automatically from previous push to refs/for/source`, + it('createPr() - creates change by pushing to refs/for/', async () => { + git.pushCommit.mockResolvedValueOnce(true); + const change = partial({ + _number: 123456, + current_revision: 'some-revision', + revisions: { + 'some-revision': partial({ + commit_with_footers: 'Renovate-Branch: source', + }), + }, + }); + clientMock.findChanges.mockResolvedValueOnce([change]); + const pr = await gerrit.createPr({ + sourceBranch: 'source', + targetBranch: 'target', + prTitle: 'title', + prBody: 'body', + }); + expect(pr).toHaveProperty('number', 123456); + expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ + sourceRef: 'source', + targetRef: 'refs/for/target', + files: [], + pushOptions: ['notify=NONE'], + }); + expect(clientMock.addMessage).toHaveBeenCalledExactlyOnceWith( + 123456, + 'body', + TAG_PULL_REQUEST_BODY, ); }); - it('createPr() - found existing but not created in the last 5 minutes => rejects', async () => { + it('createPr() - with autoApprove', async () => { + git.pushCommit.mockResolvedValueOnce(true); const change = partial({ _number: 123456, - created: t0.minus({ minutes: 6 }).toISO().replace('T', ' '), current_revision: 'some-revision', revisions: { 'some-revision': partial({ @@ -283,27 +291,39 @@ describe('modules/platform/gerrit/index', () => { }, }); clientMock.findChanges.mockResolvedValueOnce([change]); - await expect( - gerrit.createPr({ - sourceBranch: 'source', - targetBranch: 'target', - prTitle: 'title', - prBody: 'body', - }), - ).rejects.toThrow(/it was not created in the last 5 minutes/); + const pr = await gerrit.createPr({ + sourceBranch: 'source', + targetBranch: 'target', + prTitle: 'title', + prBody: 'body', + platformPrOptions: { + autoApprove: true, + }, + }); + expect(pr).toHaveProperty('number', 123456); + expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ + sourceRef: 'source', + targetRef: 'refs/for/target', + files: [], + pushOptions: ['notify=NONE', 'label=Code-Review+2'], + }); + expect(clientMock.addMessage).toHaveBeenCalledExactlyOnceWith( + 123456, + 'body', + TAG_PULL_REQUEST_BODY, + ); }); - it('createPr() - add body as message', async () => { + it('createPr() - with labels', async () => { + git.pushCommit.mockResolvedValueOnce(true); const change = partial({ _number: 123456, current_revision: 'some-revision', - created: t0.minus({ seconds: 30 }).toISO().replace('T', ' '), revisions: { 'some-revision': partial({ commit_with_footers: 'Renovate-Branch: source', }), }, - messages: [], }); clientMock.findChanges.mockResolvedValueOnce([change]); const pr = await gerrit.createPr({ @@ -311,17 +331,50 @@ describe('modules/platform/gerrit/index', () => { targetBranch: 'target', prTitle: 'title', prBody: 'body', - platformPrOptions: { - autoApprove: false, - }, + labels: ['label1', 'label2'], }); expect(pr).toHaveProperty('number', 123456); + expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ + sourceRef: 'source', + targetRef: 'refs/for/target', + files: [], + pushOptions: ['notify=NONE', 'hashtag=label1', 'hashtag=label2'], + }); expect(clientMock.addMessage).toHaveBeenCalledExactlyOnceWith( 123456, 'body', TAG_PULL_REQUEST_BODY, ); }); + + it('createPr() - no change found after push => rejects', async () => { + git.pushCommit.mockResolvedValueOnce(true); + clientMock.findChanges.mockResolvedValueOnce([]); + await expect( + gerrit.createPr({ + sourceBranch: 'source', + targetBranch: 'target', + prTitle: 'title', + prBody: 'body', + }), + ).rejects.toThrow( + `Could not find the Gerrit change after pushing to refs/for/target`, + ); + }); + + it('createPr() - push fails => rejects', async () => { + git.pushCommit.mockResolvedValueOnce(false); + await expect( + gerrit.createPr({ + sourceBranch: 'source', + targetBranch: 'target', + prTitle: 'title', + prBody: 'body', + }), + ).rejects.toThrow( + `Failed to push commit to refs/for/target to create Gerrit change`, + ); + }); }); describe('getBranchPr()', () => { diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index 13c02d89cdc..9ec6ecee44e 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -1,5 +1,4 @@ import { isTruthy } from '@sindresorhus/is'; -import { DateTime } from 'luxon'; import { logger } from '../../../logger'; import type { BranchStatus } from '../../../types'; import { parseJson } from '../../../util/common'; @@ -186,6 +185,34 @@ export async function createPr(prConfig: CreatePRConfig): Promise { prConfig.labels?.toString() ?? '' })`, ); + + logger.debug( + `Pushing commit to refs/for/${prConfig.targetBranch} to create Gerrit change`, + ); + const pushOptions = ['notify=NONE']; + if (prConfig.platformPrOptions?.autoApprove) { + pushOptions.push('label=Code-Review+2'); + } + if (prConfig.labels) { + for (const label of prConfig.labels) { + pushOptions.push(`hashtag=${label}`); + } + } + + const pushResult = await git.pushCommit({ + sourceRef: prConfig.sourceBranch, + targetRef: `refs/for/${prConfig.targetBranch}`, + files: [], + pushOptions, + }); + + if (!pushResult) { + throw new Error( + `Failed to push commit to refs/for/${prConfig.targetBranch} to create Gerrit change`, + ); + } + + // Now find the newly created change const change = ( await client.findChanges(config.repository!, { branchName: prConfig.sourceBranch, @@ -197,14 +224,7 @@ export async function createPr(prConfig: CreatePRConfig): Promise { ).pop(); if (change === undefined) { throw new Error( - `the change should be created automatically from previous push to refs/for/${prConfig.sourceBranch}`, - ); - } - const created = DateTime.fromISO(change.created.replace(' ', 'T'), {}); - const fiveMinutesAgo = DateTime.utc().minus({ minutes: 5 }); - if (created < fiveMinutesAgo) { - throw new Error( - `the change should have been created automatically from previous push to refs/for/${prConfig.sourceBranch}, but it was not created in the last 5 minutes (${change.created})`, + `Could not find the Gerrit change after pushing to refs/for/${prConfig.targetBranch}`, ); } await client.addMessage( diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 09cf68d33df..f6f76425bed 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -277,8 +277,8 @@ describe('modules/platform/gerrit/scm', () => { }); }); - describe('commitFiles()', () => { - it('commitFiles() - empty commit', async () => { + describe('commitAndPush()', () => { + it('commitAndPush() - empty commit', async () => { clientMock.findChanges.mockResolvedValueOnce([]); git.prepareCommit.mockResolvedValueOnce(null); //empty commit @@ -303,14 +303,13 @@ describe('modules/platform/gerrit/scm', () => { ); }); - it('commitFiles() - create first Patch', async () => { + it('commitAndPush() - create first commit', async () => { clientMock.findChanges.mockResolvedValueOnce([]); git.prepareCommit.mockResolvedValueOnce({ commitSha: 'commitSha' as LongCommitSha, parentCommitSha: 'parentSha' as LongCommitSha, files: [], }); - git.pushCommit.mockResolvedValueOnce(true); expect( await gerritScm.commitAndPush({ @@ -334,56 +333,11 @@ describe('modules/platform/gerrit/scm', () => { prTitle: 'pr title', force: true, }); - expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ - files: [], - sourceRef: 'renovate/dependency-1.x', - targetRef: 'refs/for/main', - pushOptions: ['notify=NONE'], - }); + // For new changes, push should NOT be called - it will be done by createPr() + expect(git.pushCommit).not.toHaveBeenCalled(); }); - it('commitFiles() - create first Patch - auto approve', async () => { - clientMock.findChanges.mockResolvedValueOnce([]); - git.prepareCommit.mockResolvedValueOnce({ - commitSha: 'commitSha' as LongCommitSha, - parentCommitSha: 'parentSha' as LongCommitSha, - files: [], - }); - git.pushCommit.mockResolvedValueOnce(true); - - expect( - await gerritScm.commitAndPush({ - branchName: 'renovate/dependency-1.x', - baseBranch: 'main', - message: 'commit msg', - files: [], - prTitle: 'pr title', - autoApprove: true, - }), - ).toBe('commitSha'); - expect(git.prepareCommit).toHaveBeenCalledExactlyOnceWith({ - baseBranch: 'main', - branchName: 'renovate/dependency-1.x', - files: [], - message: [ - 'pr title', - expect.stringMatching( - /^Renovate-Branch: renovate\/dependency-1\.x\nChange-Id: I[a-z0-9]{40}$/, - ), - ], - prTitle: 'pr title', - autoApprove: true, - force: true, - }); - expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ - files: [], - sourceRef: 'renovate/dependency-1.x', - targetRef: 'refs/for/main', - pushOptions: ['notify=NONE', 'label=Code-Review+2'], - }); - }); - - it('commitFiles() - existing change-set without new changes', async () => { + it('commitAndPush() - existing change without new changes', async () => { const existingChange = partial({ change_id: '...', current_revision: 'commitSha', @@ -426,7 +380,7 @@ describe('modules/platform/gerrit/scm', () => { expect(git.pushCommit).toHaveBeenCalledTimes(0); }); - it('commitFiles() - existing change-set with new changes - auto-approve again', async () => { + it('commitAndPush() - existing change with new changes - auto-approve', async () => { const existingChange = partial({ _number: 123456, change_id: '...', @@ -477,55 +431,7 @@ describe('modules/platform/gerrit/scm', () => { }); }); - it('commitFiles() - create first patch - with labels', async () => { - clientMock.findChanges.mockResolvedValueOnce([]); - git.prepareCommit.mockResolvedValueOnce({ - commitSha: 'commitSha' as LongCommitSha, - parentCommitSha: 'parentSha' as LongCommitSha, - files: [], - }); - git.pushCommit.mockResolvedValueOnce(true); - - expect( - await gerritScm.commitAndPush({ - branchName: 'renovate/dependency-1.x', - baseBranch: 'main', - message: 'commit msg', - files: [], - prTitle: 'pr title', - autoApprove: true, - labels: ['hashtag1', 'hashtag2'], - }), - ).toBe('commitSha'); - expect(git.prepareCommit).toHaveBeenCalledExactlyOnceWith({ - baseBranch: 'main', - branchName: 'renovate/dependency-1.x', - files: [], - message: [ - 'pr title', - expect.stringMatching( - /^Renovate-Branch: renovate\/dependency-1\.x\nChange-Id: I[a-z0-9]{40}$/, - ), - ], - prTitle: 'pr title', - autoApprove: true, - force: true, - labels: ['hashtag1', 'hashtag2'], - }); - expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ - files: [], - sourceRef: 'renovate/dependency-1.x', - targetRef: 'refs/for/main', - pushOptions: [ - 'notify=NONE', - 'label=Code-Review+2', - 'hashtag=hashtag1', - 'hashtag=hashtag2', - ], - }); - }); - - it('commitFiles() - existing change-set with new changes - ensure labels', async () => { + it('commitAndPush() - existing change with new changes - ensure labels', async () => { const existingChange = partial({ _number: 123456, change_id: '...', diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index f9b4e7ffe1a..6fb15712f37 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -145,26 +145,33 @@ export class GerritScm extends DefaultGitScm { const fetchRefSpec = currentRevision.ref; await git.fetchRevSpec(fetchRefSpec); // fetch current ChangeSet for git diff hasChanges = await git.hasDiff('HEAD', 'FETCH_HEAD'); // avoid pushing empty patch sets - } - if (hasChanges || commit.force) { - const pushOptions = ['notify=NONE']; - if (commit.autoApprove) { - pushOptions.push('label=Code-Review+2'); - } - if (commit.labels) { - for (const label of commit.labels) { - pushOptions.push(`hashtag=${label}`); + // Only push to refs/for/ when updating an existing change + if (hasChanges || commit.force) { + const pushOptions = ['notify=NONE']; + if (commit.autoApprove) { + pushOptions.push('label=Code-Review+2'); + } + if (commit.labels) { + for (const label of commit.labels) { + pushOptions.push(`hashtag=${label}`); + } + } + const pushResult = await git.pushCommit({ + sourceRef: commit.branchName, + targetRef: `refs/for/${commit.baseBranch!}`, + files: commit.files, + pushOptions, + }); + if (pushResult) { + return commitSha; } } - const pushResult = await git.pushCommit({ - sourceRef: commit.branchName, - targetRef: `refs/for/${commit.baseBranch!}`, - files: commit.files, - pushOptions, - }); - if (pushResult) { - return commitSha; - } + } else { + // The push will be done by createPr() to actually create the Gerrit change + logger.debug( + `Commit prepared for new change on branch ${commit.branchName}, but not pushed to refs/for/ yet`, + ); + return commitSha; } } return null; // empty commit, no changes in this Gerrit Change From 0b491aa256125893027c06634b4f92f4e981b959 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Thu, 13 Nov 2025 23:51:47 -0300 Subject: [PATCH 02/35] Refactor hashtags handling --- lib/modules/platform/gerrit/client.spec.ts | 40 ++++++++++++- lib/modules/platform/gerrit/client.ts | 20 +++++-- lib/modules/platform/gerrit/index.spec.ts | 50 ++++++++++++++-- lib/modules/platform/gerrit/index.ts | 8 ++- lib/modules/platform/gerrit/scm.spec.ts | 58 ------------------- lib/modules/platform/gerrit/scm.ts | 5 -- lib/modules/platform/gerrit/types.ts | 5 ++ .../config-migration/branch/create.spec.ts | 12 ++-- .../config-migration/branch/create.ts | 3 +- .../config-migration/branch/rebase.spec.ts | 2 +- .../config-migration/branch/rebase.ts | 3 +- .../onboarding/branch/create.spec.ts | 12 ---- .../repository/onboarding/branch/create.ts | 2 +- .../repository/onboarding/branch/rebase.ts | 2 +- .../repository/update/branch/commit.spec.ts | 2 - .../repository/update/branch/commit.ts | 2 - 16 files changed, 123 insertions(+), 103 deletions(-) diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index f37d17faa06..267e237255e 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -561,15 +561,49 @@ describe('modules/platform/gerrit/client', () => { }); }); - describe('deleteHashtag()', () => { - it('deleteHashtag', async () => { + describe('setHashtags()', () => { + it('add hashtags', async () => { + httpMock + .scope(gerritEndpointUrl) + .post('/a/changes/123456/hashtags', { + add: ['hashtag1', 'hashtag2'], + }) + .reply(200, gerritRestResponse([]), jsonResultHeader); + await expect( + client.setHashtags(123456, { add: ['hashtag1', 'hashtag2'] }), + ).toResolve(); + }); + + it('remove hashtags', async () => { httpMock .scope(gerritEndpointUrl) .post('/a/changes/123456/hashtags', { remove: ['hashtag1'], }) .reply(200, gerritRestResponse([]), jsonResultHeader); - await expect(client.deleteHashtag(123456, 'hashtag1')).toResolve(); + await expect( + client.setHashtags(123456, { remove: ['hashtag1'] }), + ).toResolve(); + }); + + it('add and remove hashtags in single call', async () => { + httpMock + .scope(gerritEndpointUrl) + .post('/a/changes/123456/hashtags', { + add: ['hashtag2', 'hashtag3'], + remove: ['hashtag1'], + }) + .reply(200, gerritRestResponse([]), jsonResultHeader); + await expect( + client.setHashtags(123456, { + add: ['hashtag2', 'hashtag3'], + remove: ['hashtag1'], + }), + ).toResolve(); + }); + + it('does nothing when no hashtags provided', async () => { + await expect(client.setHashtags(123456, {})).toResolve(); }); }); diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index 12959bbf298..99c47c183a1 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -8,6 +8,7 @@ import type { GerritChange, GerritChangeMessageInfo, GerritFindPRConfig, + GerritHashtagsInput, GerritMergeableInfo, GerritProjectInfo, GerritRequestDetail, @@ -185,10 +186,21 @@ class GerritClient { ); } - async deleteHashtag(changeNumber: number, hashtag: string): Promise { - await this.gerritHttp.postJson(`a/changes/${changeNumber}/hashtags`, { - body: { remove: [hashtag] }, - }); + async setHashtags( + changeNumber: number, + hashtagsInput: GerritHashtagsInput, + ): Promise { + if (hashtagsInput.add?.length === 0) { + delete hashtagsInput.add; + } + if (hashtagsInput.remove?.length === 0) { + delete hashtagsInput.remove; + } + if (hashtagsInput.add || hashtagsInput.remove) { + await this.gerritHttp.postJson(`a/changes/${changeNumber}/hashtags`, { + body: hashtagsInput, + }); + } } async addReviewers(changeNumber: number, reviewers: string[]): Promise { diff --git a/lib/modules/platform/gerrit/index.spec.ts b/lib/modules/platform/gerrit/index.spec.ts index cf3aed37025..723d76859b7 100644 --- a/lib/modules/platform/gerrit/index.spec.ts +++ b/lib/modules/platform/gerrit/index.spec.ts @@ -244,6 +244,47 @@ describe('modules/platform/gerrit/index', () => { TAG_PULL_REQUEST_BODY, ); }); + + it('updatePr() - with addLabels => add hashtags', async () => { + const change = partial({}); + clientMock.getChange.mockResolvedValueOnce(change); + await gerrit.updatePr({ + number: 123456, + prTitle: change.subject, + addLabels: ['label1', 'label2'], + }); + expect(clientMock.setHashtags).toHaveBeenCalledExactlyOnceWith(123456, { + add: ['label1', 'label2'], + }); + }); + + it('updatePr() - with removeLabels => remove hashtags', async () => { + const change = partial({}); + clientMock.getChange.mockResolvedValueOnce(change); + await gerrit.updatePr({ + number: 123456, + prTitle: change.subject, + removeLabels: ['old-label'], + }); + expect(clientMock.setHashtags).toHaveBeenCalledExactlyOnceWith(123456, { + remove: ['old-label'], + }); + }); + + it('updatePr() - with addLabels and removeLabels => update hashtags in single call', async () => { + const change = partial({}); + clientMock.getChange.mockResolvedValueOnce(change); + await gerrit.updatePr({ + number: 123456, + prTitle: change.subject, + addLabels: ['new-label'], + removeLabels: ['old-label'], + }); + expect(clientMock.setHashtags).toHaveBeenCalledExactlyOnceWith(123456, { + add: ['new-label'], + remove: ['old-label'], + }); + }); }); describe('createPr()', () => { @@ -780,11 +821,10 @@ describe('modules/platform/gerrit/index', () => { it('deleteLabel() - deletes a label', async () => { const pro = gerrit.deleteLabel(123456, 'hashtag1'); await expect(pro).resolves.toBeUndefined(); - expect(clientMock.deleteHashtag).toHaveBeenCalledTimes(1); - expect(clientMock.deleteHashtag).toHaveBeenCalledExactlyOnceWith( - 123456, - 'hashtag1', - ); + expect(clientMock.setHashtags).toHaveBeenCalledTimes(1); + expect(clientMock.setHashtags).toHaveBeenCalledExactlyOnceWith(123456, { + remove: ['hashtag1'], + }); }); }); diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index 9ec6ecee44e..2b7f03c3a4a 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -174,6 +174,12 @@ export async function updatePr(prConfig: UpdatePrConfig): Promise { TAG_PULL_REQUEST_BODY, ); } + if (prConfig.addLabels?.length || prConfig.removeLabels?.length) { + await client.setHashtags(prConfig.number, { + add: prConfig.addLabels ?? undefined, + remove: prConfig.removeLabels ?? undefined, + }); + } if (prConfig.state && prConfig.state === 'closed') { await client.abandonChange(prConfig.number); } @@ -490,7 +496,7 @@ export async function deleteLabel( number: number, label: string, ): Promise { - await client.deleteHashtag(number, label); + await client.setHashtags(number, { remove: [label] }); } export function ensureCommentRemoval( diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index f6f76425bed..31bd5f8780a 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -430,63 +430,5 @@ describe('modules/platform/gerrit/scm', () => { pushOptions: ['notify=NONE', 'label=Code-Review+2'], }); }); - - it('commitAndPush() - existing change with new changes - ensure labels', async () => { - const existingChange = partial({ - _number: 123456, - change_id: '...', - current_revision: 'commitSha', - revisions: { - commitSha: partial({ ref: 'refs/changes/1/2' }), - }, - }); - clientMock.findChanges.mockResolvedValueOnce([existingChange]); - git.prepareCommit.mockResolvedValueOnce({ - commitSha: 'commitSha' as LongCommitSha, - parentCommitSha: 'parentSha' as LongCommitSha, - files: [], - }); - git.pushCommit.mockResolvedValueOnce(true); - git.hasDiff.mockResolvedValueOnce(true); - - expect( - await gerritScm.commitAndPush({ - branchName: 'renovate/dependency-1.x', - baseBranch: 'main', - message: 'commit msg', - files: [], - prTitle: 'pr title', - autoApprove: true, - labels: ['hashtag1', 'hashtag2'], - }), - ).toBe('commitSha'); - expect(git.prepareCommit).toHaveBeenCalledExactlyOnceWith({ - baseBranch: 'main', - branchName: 'renovate/dependency-1.x', - files: [], - message: [ - 'pr title', - 'Renovate-Branch: renovate/dependency-1.x\nChange-Id: ...', - ], - prTitle: 'pr title', - autoApprove: true, - force: true, - labels: ['hashtag1', 'hashtag2'], - }); - expect(git.fetchRevSpec).toHaveBeenCalledExactlyOnceWith( - 'refs/changes/1/2', - ); - expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ - files: [], - sourceRef: 'renovate/dependency-1.x', - targetRef: 'refs/for/main', - pushOptions: [ - 'notify=NONE', - 'label=Code-Review+2', - 'hashtag=hashtag1', - 'hashtag=hashtag2', - ], - }); - }); }); }); diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index 6fb15712f37..f45520936b7 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -151,11 +151,6 @@ export class GerritScm extends DefaultGitScm { if (commit.autoApprove) { pushOptions.push('label=Code-Review+2'); } - if (commit.labels) { - for (const label of commit.labels) { - pushOptions.push(`hashtag=${label}`); - } - } const pushResult = await git.pushCommit({ sourceRef: commit.branchName, targetRef: `refs/for/${commit.baseBranch!}`, diff --git a/lib/modules/platform/gerrit/types.ts b/lib/modules/platform/gerrit/types.ts index 2aa9f9a43d0..dc472358ac8 100644 --- a/lib/modules/platform/gerrit/types.ts +++ b/lib/modules/platform/gerrit/types.ts @@ -143,3 +143,8 @@ export interface GerritMergeableInfo { | 'CHERRY_PICK'; mergeable: boolean; } + +export interface GerritHashtagsInput { + add?: string[]; + remove?: string[]; +} diff --git a/lib/workers/repository/config-migration/branch/create.spec.ts b/lib/workers/repository/config-migration/branch/create.spec.ts index 0e5255f1c7a..f89d129937a 100644 --- a/lib/workers/repository/config-migration/branch/create.spec.ts +++ b/lib/workers/repository/config-migration/branch/create.spec.ts @@ -51,7 +51,7 @@ describe('workers/repository/config-migration/branch/create', () => { message: 'Migrate config renovate.json', platformCommit: 'auto', force: true, - labels: [], + prTitle: 'Migrate Renovate config', }); }); @@ -78,7 +78,7 @@ describe('workers/repository/config-migration/branch/create', () => { message, platformCommit: 'auto', force: true, - labels: [], + prTitle: message, }); }); @@ -117,7 +117,7 @@ describe('workers/repository/config-migration/branch/create', () => { message: 'Migrate config renovate.json', platformCommit: 'auto', force: true, - labels: [], + prTitle: 'Migrate Renovate config', }); }); @@ -145,7 +145,7 @@ describe('workers/repository/config-migration/branch/create', () => { message, platformCommit: 'auto', force: true, - labels: [], + prTitle: 'PREFIX: migrate Renovate config', }); }); }); @@ -175,7 +175,7 @@ describe('workers/repository/config-migration/branch/create', () => { message, platformCommit: 'auto', force: true, - labels: [], + prTitle: `${prefix}: migrate Renovate config`, }); }); @@ -204,7 +204,7 @@ describe('workers/repository/config-migration/branch/create', () => { message, platformCommit: 'auto', force: true, - labels: [], + prTitle: `${prefix}: migrate Renovate config`, }); }); }); diff --git a/lib/workers/repository/config-migration/branch/create.ts b/lib/workers/repository/config-migration/branch/create.ts index 9147248e6b4..ba61b5b0ba9 100644 --- a/lib/workers/repository/config-migration/branch/create.ts +++ b/lib/workers/repository/config-migration/branch/create.ts @@ -74,6 +74,7 @@ export async function createConfigMigrationBranch( message: commitMessage.toString(), platformCommit: config.platformCommit, force: true, - labels: config.labels, + // Only needed by Gerrit platform + prTitle: commitMessageFactory.getPrTitle(), }); } diff --git a/lib/workers/repository/config-migration/branch/rebase.spec.ts b/lib/workers/repository/config-migration/branch/rebase.spec.ts index c95e13fc8a4..eb7fbb87107 100644 --- a/lib/workers/repository/config-migration/branch/rebase.spec.ts +++ b/lib/workers/repository/config-migration/branch/rebase.spec.ts @@ -110,7 +110,7 @@ describe('workers/repository/config-migration/branch/rebase', () => { message: `Migrate config ${filename}`, platformCommit: 'auto', baseBranch: 'dev', - labels: [], + prTitle: 'Migrate Renovate config', }); }, ); diff --git a/lib/workers/repository/config-migration/branch/rebase.ts b/lib/workers/repository/config-migration/branch/rebase.ts index de64b33bd01..aa7ada3c290 100644 --- a/lib/workers/repository/config-migration/branch/rebase.ts +++ b/lib/workers/repository/config-migration/branch/rebase.ts @@ -54,7 +54,8 @@ export async function rebaseMigrationBranch( ], message: commitMessage.toString(), platformCommit: config.platformCommit, - labels: config.labels, + // Only needed by Gerrit platform + prTitle: commitMessageFactory.getPrTitle(), }); } diff --git a/lib/workers/repository/onboarding/branch/create.spec.ts b/lib/workers/repository/onboarding/branch/create.spec.ts index a9f62ca04e5..057d97ef50d 100644 --- a/lib/workers/repository/onboarding/branch/create.spec.ts +++ b/lib/workers/repository/onboarding/branch/create.spec.ts @@ -32,7 +32,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message: 'Add renovate.json', platformCommit: 'auto', - labels: [], }); }); @@ -56,7 +55,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', - labels: [], }); }); @@ -78,7 +76,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message: `Add renovate.json\n\nsome commit body`, platformCommit: 'auto', - labels: [], }); }); @@ -105,7 +102,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message: `We can Renovate if we want to, we can leave PRs in decline\n\nSigned Off: `, platformCommit: 'auto', - labels: [], }); }); }); @@ -131,7 +127,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', - labels: [], }); }); @@ -160,7 +155,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', - labels: [], }); }); }); @@ -186,7 +180,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', - labels: [], }); }); @@ -215,7 +208,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', - labels: [], }); }); }); @@ -242,7 +234,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', - labels: [], }); }); @@ -267,7 +258,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', - labels: [], }); }); @@ -293,7 +283,6 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', - labels: [], }); }); @@ -313,7 +302,6 @@ describe('workers/repository/onboarding/branch/create', () => { message, force: true, platformCommit: 'auto', - labels: [], }); }); }); diff --git a/lib/workers/repository/onboarding/branch/create.ts b/lib/workers/repository/onboarding/branch/create.ts index b8ce6181a3d..e691881a3e3 100644 --- a/lib/workers/repository/onboarding/branch/create.ts +++ b/lib/workers/repository/onboarding/branch/create.ts @@ -52,6 +52,6 @@ export async function createOnboardingBranch( message: commitMessage, platformCommit: config.platformCommit, force: true, - labels: config.labels, + // TODO: add prTitle for Gerrit }); } diff --git a/lib/workers/repository/onboarding/branch/rebase.ts b/lib/workers/repository/onboarding/branch/rebase.ts index 9706acf7f67..afca37bf852 100644 --- a/lib/workers/repository/onboarding/branch/rebase.ts +++ b/lib/workers/repository/onboarding/branch/rebase.ts @@ -59,6 +59,6 @@ export async function rebaseOnboardingBranch( ], message: commitMessage.toString(), platformCommit: config.platformCommit, - labels: config.labels, + // TODO: add prTitle for Gerrit }); } diff --git a/lib/workers/repository/update/branch/commit.spec.ts b/lib/workers/repository/update/branch/commit.spec.ts index 898dd7fa1bf..cbc5b0d4833 100644 --- a/lib/workers/repository/update/branch/commit.spec.ts +++ b/lib/workers/repository/update/branch/commit.spec.ts @@ -21,7 +21,6 @@ describe('workers/repository/update/branch/commit', () => { updatedArtifacts: [], upgrades: [], platformCommit: 'auto', - labels: ['label1', 'label2'], } satisfies BranchConfig; scm.commitAndPush.mockResolvedValueOnce('123test' as LongCommitSha); GlobalConfig.reset(); @@ -55,7 +54,6 @@ describe('workers/repository/update/branch/commit', () => { force: false, message: 'some commit message', platformCommit: 'auto', - labels: ['label1', 'label2'], }, ], ]); diff --git a/lib/workers/repository/update/branch/commit.ts b/lib/workers/repository/update/branch/commit.ts index cf6186f2502..bf440ed936e 100644 --- a/lib/workers/repository/update/branch/commit.ts +++ b/lib/workers/repository/update/branch/commit.ts @@ -64,7 +64,5 @@ export function commitFilesToBranch( prTitle: config.prTitle, // Only needed by Gerrit platform autoApprove: config.autoApprove, - // Only needed by Gerrit platform - labels: config.labels, }); } From e512e13be2127daf299e74335b5360c526fe51a2 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Sun, 14 Dec 2025 13:46:46 -0300 Subject: [PATCH 03/35] Add prTitle to all missing commitAndPush() --- .../repository/onboarding/branch/create.spec.ts | 12 ++++++++++++ lib/workers/repository/onboarding/branch/create.ts | 10 ++++++++-- .../repository/onboarding/branch/rebase.spec.ts | 13 +++++++++++++ lib/workers/repository/onboarding/branch/rebase.ts | 10 ++++++++-- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/workers/repository/onboarding/branch/create.spec.ts b/lib/workers/repository/onboarding/branch/create.spec.ts index 057d97ef50d..e9c79c7cb43 100644 --- a/lib/workers/repository/onboarding/branch/create.spec.ts +++ b/lib/workers/repository/onboarding/branch/create.spec.ts @@ -32,6 +32,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message: 'Add renovate.json', platformCommit: 'auto', + prTitle: 'Configure Renovate', }); }); @@ -55,6 +56,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', + prTitle: 'Configure Renovate', }); }); @@ -76,6 +78,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message: `Add renovate.json\n\nsome commit body`, platformCommit: 'auto', + prTitle: 'Configure Renovate', }); }); @@ -102,6 +105,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message: `We can Renovate if we want to, we can leave PRs in decline\n\nSigned Off: `, platformCommit: 'auto', + prTitle: 'Configure Renovate', }); }); }); @@ -127,6 +131,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', + prTitle: 'Configure Renovate', }); }); @@ -155,6 +160,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', + prTitle: 'Configure Renovate', }); }); }); @@ -180,6 +186,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', + prTitle: 'chore: Configure Renovate', }); }); @@ -208,6 +215,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', + prTitle: 'chore: Configure Renovate', }); }); }); @@ -234,6 +242,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', + prTitle: 'chore: Configure Renovate', }); }); @@ -258,6 +267,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', + prTitle: 'chore: Configure Renovate', }); }); @@ -283,6 +293,7 @@ describe('workers/repository/onboarding/branch/create', () => { force: true, message, platformCommit: 'auto', + prTitle: 'chore: Configure Renovate', }); }); @@ -302,6 +313,7 @@ describe('workers/repository/onboarding/branch/create', () => { message, force: true, platformCommit: 'auto', + prTitle: 'chore: Configure Renovate', }); }); }); diff --git a/lib/workers/repository/onboarding/branch/create.ts b/lib/workers/repository/onboarding/branch/create.ts index e691881a3e3..f8e9ca142d7 100644 --- a/lib/workers/repository/onboarding/branch/create.ts +++ b/lib/workers/repository/onboarding/branch/create.ts @@ -3,7 +3,7 @@ import type { RenovateConfig } from '../../../../config/types'; import { logger } from '../../../../logger'; import { scm } from '../../../../modules/platform/scm'; import { compile } from '../../../../util/template'; -import { getDefaultConfigFileName } from '../common'; +import { getDefaultConfigFileName, getSemanticCommitPrTitle } from '../common'; import { OnboardingCommitMessageFactory } from './commit-message'; import { getOnboardingConfigContents } from './config'; @@ -38,6 +38,11 @@ export async function createOnboardingBranch( return null; } + const prTitle = + config.semanticCommits === 'enabled' + ? getSemanticCommitPrTitle(config) + : config.onboardingPrTitle!; + return scm.commitAndPush({ baseBranch: config.baseBranch, branchName: config.onboardingBranch!, @@ -52,6 +57,7 @@ export async function createOnboardingBranch( message: commitMessage, platformCommit: config.platformCommit, force: true, - // TODO: add prTitle for Gerrit + // Only needed by Gerrit platform + prTitle, }); } diff --git a/lib/workers/repository/onboarding/branch/rebase.spec.ts b/lib/workers/repository/onboarding/branch/rebase.spec.ts index 0e03668f498..ff94c59abe9 100644 --- a/lib/workers/repository/onboarding/branch/rebase.spec.ts +++ b/lib/workers/repository/onboarding/branch/rebase.spec.ts @@ -35,6 +35,7 @@ describe('workers/repository/onboarding/branch/rebase', () => { $schema: 'https://docs.renovatebot.com/renovate-schema.json', }, onboardingConfigFileName: 'renovate.json', + onboardingPrTitle: 'Configure Renovate', repository: 'some/repo', }; configModule.getOnboardingConfigContents.mockResolvedValue(''); @@ -50,6 +51,9 @@ describe('workers/repository/onboarding/branch/rebase', () => { it('rebases onboarding branch', async () => { await rebaseOnboardingBranch(config, hash); expect(scm.commitAndPush).toHaveBeenCalledTimes(1); + expect(scm.commitAndPush.mock.calls[0][0].prTitle).toBe( + 'Configure Renovate', + ); }); it('uses the onboardingConfigFileName if set', async () => { @@ -67,6 +71,9 @@ describe('workers/repository/onboarding/branch/rebase', () => { expect(scm.commitAndPush.mock.calls[0][0].files[0].path).toBe( '.github/renovate.json', ); + expect(scm.commitAndPush.mock.calls[0][0].prTitle).toBe( + 'Configure Renovate', + ); }); it('falls back to "renovate.json" if onboardingConfigFileName is not set', async () => { @@ -84,11 +91,17 @@ describe('workers/repository/onboarding/branch/rebase', () => { expect(scm.commitAndPush.mock.calls[0][0].files[0].path).toBe( 'renovate.json', ); + expect(scm.commitAndPush.mock.calls[0][0].prTitle).toBe( + 'Configure Renovate', + ); }); it('handles a missing previous config hash', async () => { await rebaseOnboardingBranch(config, undefined); expect(scm.commitAndPush).toHaveBeenCalled(); + expect(scm.commitAndPush.mock.calls[0][0].prTitle).toBe( + 'Configure Renovate', + ); }); it('does nothing if config hashes match', async () => { diff --git a/lib/workers/repository/onboarding/branch/rebase.ts b/lib/workers/repository/onboarding/branch/rebase.ts index afca37bf852..eaa3b4f32ab 100644 --- a/lib/workers/repository/onboarding/branch/rebase.ts +++ b/lib/workers/repository/onboarding/branch/rebase.ts @@ -3,7 +3,7 @@ import type { RenovateConfig } from '../../../../config/types'; import { logger } from '../../../../logger'; import { scm } from '../../../../modules/platform/scm'; import { toSha256 } from '../../../../util/hash'; -import { getDefaultConfigFileName } from '../common'; +import { getDefaultConfigFileName, getSemanticCommitPrTitle } from '../common'; import { OnboardingCommitMessageFactory } from './commit-message'; import { getOnboardingConfigContents } from './config'; @@ -46,6 +46,11 @@ export async function rebaseOnboardingBranch( ); const commitMessage = commitMessageFactory.create(); + const prTitle = + config.semanticCommits === 'enabled' + ? getSemanticCommitPrTitle(config) + : config.onboardingPrTitle!; + // TODO #22198 return scm.commitAndPush({ baseBranch: config.baseBranch, @@ -59,6 +64,7 @@ export async function rebaseOnboardingBranch( ], message: commitMessage.toString(), platformCommit: config.platformCommit, - // TODO: add prTitle for Gerrit + // Only needed by Gerrit platform + prTitle, }); } From 676016abe69e67fbd0a6f78d56acb9de5c93ce3a Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 30 Jan 2026 18:16:02 -0300 Subject: [PATCH 04/35] Fix gerrit tests --- lib/modules/platform/gerrit/index.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/modules/platform/gerrit/index.spec.ts b/lib/modules/platform/gerrit/index.spec.ts index 69b0c1e0cd7..8ca0351abcc 100644 --- a/lib/modules/platform/gerrit/index.spec.ts +++ b/lib/modules/platform/gerrit/index.spec.ts @@ -360,6 +360,7 @@ describe('modules/platform/gerrit/index', () => { commit_with_footers: 'Renovate-Branch: source', }), }, + created: '2025-04-14 16:33:37.000000000', }); clientMock.findChanges.mockResolvedValueOnce([change]); const pr = await gerrit.createPr({ @@ -392,6 +393,7 @@ describe('modules/platform/gerrit/index', () => { commit_with_footers: 'Renovate-Branch: source', }), }, + created: '2025-04-14 16:33:37.000000000', }); clientMock.findChanges.mockResolvedValueOnce([change]); const pr = await gerrit.createPr({ @@ -427,6 +429,7 @@ describe('modules/platform/gerrit/index', () => { commit_with_footers: 'Renovate-Branch: source', }), }, + created: '2025-04-14 16:33:37.000000000', }); clientMock.findChanges.mockResolvedValueOnce([change]); const pr = await gerrit.createPr({ @@ -505,7 +508,7 @@ describe('modules/platform/gerrit/index', () => { commit_with_footers: 'Renovate-Branch: renovate/dependency-1.x', }), }, - created: '2025-04-14T16:33:37.000000000', + created: '2025-04-14 16:33:37.000000000', }); clientMock.getBranchChange.mockResolvedValueOnce(change); await expect( From 1ddaf28abfd09fafecca7acb9e4c6f99f97903f9 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 30 Jan 2026 18:37:42 -0300 Subject: [PATCH 05/35] Add warning when prTitle is set in updatePr --- lib/modules/platform/gerrit/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index 3a2d3f8cda6..aafc6512170 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -201,6 +201,12 @@ export async function getPr(number: number): Promise { export async function updatePr(prConfig: UpdatePrConfig): Promise { logger.debug(`updatePr(${prConfig.number}, ${prConfig.prTitle})`); + /* v8 ignore next -- should never happen */ + if (prConfig.prTitle) { + logger.warn( + 'updatePr() called with prTitle - this should never happen as the title should be set via commitAndPush(), please report this issue.', + ); + } // prConfig.prBody will only be set if the body has changed if (prConfig.prBody) { await client.addMessage( From 8d9f0c22ccbbc9cbe8b20fb4b771de0ef7e1a68b Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 30 Jan 2026 18:45:05 -0300 Subject: [PATCH 06/35] Remove now useless labels from commitAndPush --- lib/util/git/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/util/git/types.ts b/lib/util/git/types.ts index 354efe55678..5d97edb09ab 100644 --- a/lib/util/git/types.ts +++ b/lib/util/git/types.ts @@ -90,8 +90,6 @@ export interface CommitFilesConfig { prTitle?: string; /** Only needed by Gerrit platform */ autoApprove?: boolean; - /** Only needed by Gerrit platform */ - labels?: string[]; } export interface PushFilesConfig { From a64c2b30d826b22a609ae557c140a8b973a2f969 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 30 Jan 2026 19:06:41 -0300 Subject: [PATCH 07/35] Fix gerrit client coverage --- lib/modules/platform/gerrit/client.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/modules/platform/gerrit/client.spec.ts b/lib/modules/platform/gerrit/client.spec.ts index e768bb06252..ccae7fc5c54 100644 --- a/lib/modules/platform/gerrit/client.spec.ts +++ b/lib/modules/platform/gerrit/client.spec.ts @@ -758,6 +758,11 @@ describe('modules/platform/gerrit/client', () => { it('does nothing when no hashtags provided', async () => { await expect(client.setHashtags(123456, {})).toResolve(); + await expect(client.setHashtags(123456, { add: [] })).toResolve(); + await expect(client.setHashtags(123456, { remove: [] })).toResolve(); + await expect( + client.setHashtags(123456, { add: [], remove: [] }), + ).toResolve(); }); }); From 5acc7f02e921ce743d6955cdb21664e42167d665 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 30 Jan 2026 19:15:56 -0300 Subject: [PATCH 08/35] Fix gerrit scm coverage --- lib/modules/platform/gerrit/scm.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index e77021a7761..8e06581939e 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -152,15 +152,15 @@ export class GerritScm extends DefaultGitScm { } // If a change already exists, we push to the same target branch to // avoid creating a new change if the base branch has changed. - // updatePr() will take care of moving the existing change to a different base - // branch if needed. - const changeBranch = existingChange?.branch ?? commit.baseBranch!; + // updatePr() will later take care of moving the existing change to a + // different base branch if needed. const pushResult = await git.pushCommit({ sourceRef: commit.branchName, - targetRef: `refs/for/${changeBranch}`, + targetRef: `refs/for/${existingChange.branch}`, files: commit.files, pushOptions, }); + /* v8 ignore else -- should never happen */ if (pushResult) { return commitSha; } From 11009d9147c535644a40080f8cd139529a76a388 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Sat, 31 Jan 2026 19:55:24 -0300 Subject: [PATCH 09/35] Fix rebase coverage --- .../repository/onboarding/branch/rebase.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/workers/repository/onboarding/branch/rebase.spec.ts b/lib/workers/repository/onboarding/branch/rebase.spec.ts index 3c26761686c..762fc630892 100644 --- a/lib/workers/repository/onboarding/branch/rebase.spec.ts +++ b/lib/workers/repository/onboarding/branch/rebase.spec.ts @@ -121,6 +121,21 @@ describe('workers/repository/onboarding/branch/rebase', () => { expect(scm.commitAndPush).not.toHaveBeenCalled(); }); + it('uses semantic commit PR title when semanticCommits is enabled', async () => { + GlobalConfig.set({ localDir: '', platform: 'github' }); + await rebaseOnboardingBranch( + { + ...config, + semanticCommits: 'enabled', + }, + hash, + ); + expect(scm.commitAndPush).toHaveBeenCalledTimes(1); + expect(scm.commitAndPush.mock.calls[0][0].prTitle).toBe( + 'chore: Configure Renovate', + ); + }); + // does not rebase on platforms that do not support html comments it.each` platform From 2c25eb2a738a4af2d6e31d6df0cac9b3472e87c6 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Sun, 1 Feb 2026 13:31:53 -0300 Subject: [PATCH 10/35] Improve TODO about assignees --- lib/modules/platform/gerrit/client.ts | 56 +++++++++++++-------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index c2a58e33931..fc9e70c01fe 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -1,10 +1,10 @@ -import semver from 'semver'; -import { z } from 'zod'; -import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages.ts'; -import { logger } from '../../../logger/index.ts'; -import { GerritHttp } from '../../../util/http/gerrit.ts'; -import type { HttpOptions } from '../../../util/http/types.ts'; -import { getQueryString } from '../../../util/url.ts'; +import semver from "semver"; +import { z } from "zod"; +import { REPOSITORY_ARCHIVED } from "../../../constants/error-messages.ts"; +import { logger } from "../../../logger/index.ts"; +import { GerritHttp } from "../../../util/http/gerrit.ts"; +import type { HttpOptions } from "../../../util/http/types.ts"; +import { getQueryString } from "../../../util/url.ts"; import type { GerritAccountInfo, GerritBranchInfo, @@ -15,12 +15,12 @@ import type { GerritMergeableInfo, GerritProjectInfo, GerritRequestDetail, -} from './types.ts'; +} from "./types.ts"; import { MAX_GERRIT_COMMENT_SIZE, MIN_GERRIT_VERSION, mapPrStateToGerritFilter, -} from './utils.ts'; +} from "./utils.ts"; class GerritClient { // memCache is disabled because GerritPrCache will provide a smarter caching @@ -32,10 +32,10 @@ class GerritClient { } async getGerritVersion( - options: Pick, + options: Pick, ): Promise { const res = await this.gerritHttp.getJson( - 'a/config/server/version', + "a/config/server/version", options, z.string(), ); @@ -44,7 +44,7 @@ class GerritClient { async getRepos(): Promise { const res = await this.gerritHttp.getJsonUnchecked( - 'a/projects/?type=CODE&state=ACTIVE', + "a/projects/?type=CODE&state=ACTIVE", ); return Object.keys(res.body); } @@ -54,7 +54,7 @@ class GerritClient { await this.gerritHttp.getJsonUnchecked( `a/projects/${encodeURIComponent(repository)}`, ); - if (projectInfo.body.state !== 'ACTIVE') { + if (projectInfo.body.state !== "ACTIVE") { throw new Error(REPOSITORY_ARCHIVED); } return projectInfo.body; @@ -71,7 +71,7 @@ class GerritClient { repository: string, config: Pick< GerritFindPRConfig, - 'branchName' | 'state' | 'targetBranch' | 'requestDetails' + "branchName" | "state" | "targetBranch" | "requestDetails" >, ): Promise { const changes = await this.findChanges(repository, { @@ -123,7 +123,7 @@ class GerritClient { while (true) { query.S = allChanges.length + startOffset; - const queryString = `q=${filters.join('+')}&${getQueryString(query)}`; + const queryString = `q=${filters.join("+")}&${getQueryString(query)}`; const changes = await this.gerritHttp.getJsonUnchecked( `a/changes/?${queryString}`, ); @@ -176,7 +176,7 @@ class GerritClient { await this.gerritHttp.postJson(`a/changes/${changeNumber}/abandon`, { body: { message, - notify: 'OWNER_REVIEWERS', // Avoids notifying cc's + notify: "OWNER_REVIEWERS", // Avoids notifying cc's }, }); } @@ -203,7 +203,7 @@ class GerritClient { const message = this.normalizeMessage(fullMessage); await this.gerritHttp.postJson( `a/changes/${changeNumber}/revisions/current/review`, - { body: { message, tag, notify: 'NONE' } }, + { body: { message, tag, notify: "NONE" } }, ); } @@ -239,7 +239,7 @@ class GerritClient { ): Promise { await this.gerritHttp.postJson( `a/changes/${changeNumber}/revisions/current/review`, - { body: { labels: { [label]: value }, notify: 'NONE' } }, + { body: { labels: { [label]: value }, notify: "NONE" } }, ); } @@ -266,7 +266,7 @@ class GerritClient { { body: { reviewers: reviewers.map((r) => ({ reviewer: r })), - notify: 'OWNER_REVIEWERS', // Avoids notifying cc's + notify: "OWNER_REVIEWERS", // Avoids notifying cc's }, }, ); @@ -274,7 +274,7 @@ class GerritClient { async addAssignee(changeNumber: number, assignee: string): Promise { await this.gerritHttp.putJson( - // TODO: refactor this as this API removed in Gerrit 3.8 + // TODO: use CCs instead as Gerrit 3.8+ no longer supports assignees `a/changes/${changeNumber}/assignee`, { body: { assignee }, @@ -292,7 +292,7 @@ class GerritClient { repo, )}/branches/${encodeURIComponent(branch)}/files/${encodeURIComponent(fileName)}/content`, ); - return Buffer.from(base64Content.body, 'base64').toString(); + return Buffer.from(base64Content.body, "base64").toString(); } async moveChange( @@ -317,7 +317,7 @@ class GerritClient { const encoder = new TextEncoder(); const bytes = encoder.encode(msg); if (bytes.length > MAX_GERRIT_COMMENT_SIZE) { - const truncationNotice = '\n\n[Truncated by Renovate]'; + const truncationNotice = "\n\n[Truncated by Renovate]"; const truncationNoticeBytes = encoder.encode(truncationNotice); const maxContentBytes = MAX_GERRIT_COMMENT_SIZE - truncationNoticeBytes.length; @@ -334,10 +334,10 @@ class GerritClient { searchConfig: GerritFindPRConfig, ): string[] { const filters = [ - 'owner:self', + "owner:self", `project:${repository}`, - '-is:wip', - '-is:private', + "-is:wip", + "-is:private", ]; const filterState = mapPrStateToGerritFilter(searchConfig.state); if (filterState) { @@ -345,8 +345,8 @@ class GerritClient { } if (searchConfig.branchName) { filters.push(`footer:Renovate-Branch=${searchConfig.branchName}`); - } else if (semver.gte(this.gerritVersion, '3.6.0')) { - filters.push('hasfooter:Renovate-Branch'); + } else if (semver.gte(this.gerritVersion, "3.6.0")) { + filters.push("hasfooter:Renovate-Branch"); } else { filters.push('message:"Renovate-Branch: "'); } @@ -360,7 +360,7 @@ class GerritClient { // Quotes in the search operators must be escaped with a backslash: // https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators const escapedTitle = searchConfig.prTitle.replaceAll('"', '\\"'); - if (semver.gte(this.gerritVersion, '3.8.0')) { + if (semver.gte(this.gerritVersion, "3.8.0")) { filters.push(`subject:"${escapedTitle}"`); } else { filters.push(`message:"${escapedTitle}"`); From 082468ca28091075b9820a85a8e925b487fa001c Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Sun, 1 Feb 2026 22:16:38 -0300 Subject: [PATCH 11/35] Fix formatting --- lib/modules/platform/gerrit/client.ts | 54 +++++++++++++-------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index fc9e70c01fe..7eccda7c808 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -1,10 +1,10 @@ -import semver from "semver"; -import { z } from "zod"; -import { REPOSITORY_ARCHIVED } from "../../../constants/error-messages.ts"; -import { logger } from "../../../logger/index.ts"; -import { GerritHttp } from "../../../util/http/gerrit.ts"; -import type { HttpOptions } from "../../../util/http/types.ts"; -import { getQueryString } from "../../../util/url.ts"; +import semver from 'semver'; +import { z } from 'zod'; +import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages.ts'; +import { logger } from '../../../logger/index.ts'; +import { GerritHttp } from '../../../util/http/gerrit.ts'; +import type { HttpOptions } from '../../../util/http/types.ts'; +import { getQueryString } from '../../../util/url.ts'; import type { GerritAccountInfo, GerritBranchInfo, @@ -15,12 +15,12 @@ import type { GerritMergeableInfo, GerritProjectInfo, GerritRequestDetail, -} from "./types.ts"; +} from './types.ts'; import { MAX_GERRIT_COMMENT_SIZE, MIN_GERRIT_VERSION, mapPrStateToGerritFilter, -} from "./utils.ts"; +} from './utils.ts'; class GerritClient { // memCache is disabled because GerritPrCache will provide a smarter caching @@ -32,10 +32,10 @@ class GerritClient { } async getGerritVersion( - options: Pick, + options: Pick, ): Promise { const res = await this.gerritHttp.getJson( - "a/config/server/version", + 'a/config/server/version', options, z.string(), ); @@ -44,7 +44,7 @@ class GerritClient { async getRepos(): Promise { const res = await this.gerritHttp.getJsonUnchecked( - "a/projects/?type=CODE&state=ACTIVE", + 'a/projects/?type=CODE&state=ACTIVE', ); return Object.keys(res.body); } @@ -54,7 +54,7 @@ class GerritClient { await this.gerritHttp.getJsonUnchecked( `a/projects/${encodeURIComponent(repository)}`, ); - if (projectInfo.body.state !== "ACTIVE") { + if (projectInfo.body.state !== 'ACTIVE') { throw new Error(REPOSITORY_ARCHIVED); } return projectInfo.body; @@ -71,7 +71,7 @@ class GerritClient { repository: string, config: Pick< GerritFindPRConfig, - "branchName" | "state" | "targetBranch" | "requestDetails" + 'branchName' | 'state' | 'targetBranch' | 'requestDetails' >, ): Promise { const changes = await this.findChanges(repository, { @@ -123,7 +123,7 @@ class GerritClient { while (true) { query.S = allChanges.length + startOffset; - const queryString = `q=${filters.join("+")}&${getQueryString(query)}`; + const queryString = `q=${filters.join('+')}&${getQueryString(query)}`; const changes = await this.gerritHttp.getJsonUnchecked( `a/changes/?${queryString}`, ); @@ -176,7 +176,7 @@ class GerritClient { await this.gerritHttp.postJson(`a/changes/${changeNumber}/abandon`, { body: { message, - notify: "OWNER_REVIEWERS", // Avoids notifying cc's + notify: 'OWNER_REVIEWERS', // Avoids notifying cc's }, }); } @@ -203,7 +203,7 @@ class GerritClient { const message = this.normalizeMessage(fullMessage); await this.gerritHttp.postJson( `a/changes/${changeNumber}/revisions/current/review`, - { body: { message, tag, notify: "NONE" } }, + { body: { message, tag, notify: 'NONE' } }, ); } @@ -239,7 +239,7 @@ class GerritClient { ): Promise { await this.gerritHttp.postJson( `a/changes/${changeNumber}/revisions/current/review`, - { body: { labels: { [label]: value }, notify: "NONE" } }, + { body: { labels: { [label]: value }, notify: 'NONE' } }, ); } @@ -266,7 +266,7 @@ class GerritClient { { body: { reviewers: reviewers.map((r) => ({ reviewer: r })), - notify: "OWNER_REVIEWERS", // Avoids notifying cc's + notify: 'OWNER_REVIEWERS', // Avoids notifying cc's }, }, ); @@ -292,7 +292,7 @@ class GerritClient { repo, )}/branches/${encodeURIComponent(branch)}/files/${encodeURIComponent(fileName)}/content`, ); - return Buffer.from(base64Content.body, "base64").toString(); + return Buffer.from(base64Content.body, 'base64').toString(); } async moveChange( @@ -317,7 +317,7 @@ class GerritClient { const encoder = new TextEncoder(); const bytes = encoder.encode(msg); if (bytes.length > MAX_GERRIT_COMMENT_SIZE) { - const truncationNotice = "\n\n[Truncated by Renovate]"; + const truncationNotice = '\n\n[Truncated by Renovate]'; const truncationNoticeBytes = encoder.encode(truncationNotice); const maxContentBytes = MAX_GERRIT_COMMENT_SIZE - truncationNoticeBytes.length; @@ -334,10 +334,10 @@ class GerritClient { searchConfig: GerritFindPRConfig, ): string[] { const filters = [ - "owner:self", + 'owner:self', `project:${repository}`, - "-is:wip", - "-is:private", + '-is:wip', + '-is:private', ]; const filterState = mapPrStateToGerritFilter(searchConfig.state); if (filterState) { @@ -345,8 +345,8 @@ class GerritClient { } if (searchConfig.branchName) { filters.push(`footer:Renovate-Branch=${searchConfig.branchName}`); - } else if (semver.gte(this.gerritVersion, "3.6.0")) { - filters.push("hasfooter:Renovate-Branch"); + } else if (semver.gte(this.gerritVersion, '3.6.0')) { + filters.push('hasfooter:Renovate-Branch'); } else { filters.push('message:"Renovate-Branch: "'); } @@ -360,7 +360,7 @@ class GerritClient { // Quotes in the search operators must be escaped with a backslash: // https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators const escapedTitle = searchConfig.prTitle.replaceAll('"', '\\"'); - if (semver.gte(this.gerritVersion, "3.8.0")) { + if (semver.gte(this.gerritVersion, '3.8.0')) { filters.push(`subject:"${escapedTitle}"`); } else { filters.push(`message:"${escapedTitle}"`); From ea9d9023461f6dd7c9edc63c7cccfbe91a5c4297 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 3 Feb 2026 14:37:41 -0300 Subject: [PATCH 12/35] Fix review comment --- lib/modules/platform/gerrit/scm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index 8e06581939e..b88bd76a58c 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -150,7 +150,7 @@ export class GerritScm extends DefaultGitScm { if (commit.autoApprove) { pushOptions.push('label=Code-Review+2'); } - // If a change already exists, we push to the same target branch to + // Since the change already exists, we push to the same target branch to // avoid creating a new change if the base branch has changed. // updatePr() will later take care of moving the existing change to a // different base branch if needed. From 00b8f16188aa7f6199b51e58599346aeaaf4f8b4 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 3 Feb 2026 16:09:09 -0300 Subject: [PATCH 13/35] Extract to a pushForReview function --- lib/modules/platform/gerrit/index.ts | 19 ++++---------- lib/modules/platform/gerrit/scm.ts | 39 +++++++++++++++++++++------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index aafc6512170..9254aebd736 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -31,7 +31,7 @@ import { repoFingerprint } from '../util.ts'; import { smartTruncate } from '../utils/pr-body.ts'; import { readOnlyIssueBody } from '../utils/read-only-issue-body.ts'; import { client } from './client.ts'; -import { configureScm } from './scm.ts'; +import { configureScm, pushForReview } from './scm.ts'; import type { GerritLabelTypeInfo, GerritProjectInfo } from './types.ts'; import { MAX_GERRIT_COMMENT_SIZE, @@ -239,21 +239,12 @@ export async function createPr(prConfig: CreatePRConfig): Promise { logger.debug( `Pushing commit to refs/for/${prConfig.targetBranch} to create Gerrit change`, ); - const pushOptions = ['notify=NONE']; - if (prConfig.platformPrOptions?.autoApprove) { - pushOptions.push('label=Code-Review+2'); - } - if (prConfig.labels) { - for (const label of prConfig.labels) { - pushOptions.push(`hashtag=${label}`); - } - } - - const pushResult = await git.pushCommit({ + const pushResult = await pushForReview({ sourceRef: prConfig.sourceBranch, - targetRef: `refs/for/${prConfig.targetBranch}`, + targetBranch: prConfig.targetBranch, files: [], - pushOptions, + autoApprove: prConfig.platformPrOptions?.autoApprove, + labels: prConfig.labels ?? undefined, }); if (!pushResult) { diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index b88bd76a58c..c56c6ed84c3 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -3,6 +3,7 @@ import { logger } from '../../../logger/index.ts'; import * as git from '../../../util/git/index.ts'; import type { CommitFilesConfig, + FileChange, LongCommitSha, } from '../../../util/git/types.ts'; import { hash } from '../../../util/hash.ts'; @@ -17,6 +18,31 @@ export function configureScm(repo: string, login: string): void { username = login; } +export async function pushForReview(options: { + sourceRef: string; + targetBranch: string; + files: FileChange[]; + autoApprove?: boolean; + labels?: string[]; +}): Promise { + const pushOptions = ['notify=NONE']; + if (options.autoApprove) { + pushOptions.push('label=Code-Review+2'); + } + if (options.labels) { + for (const label of options.labels) { + pushOptions.push(`hashtag=${label}`); + } + } + + return await git.pushCommit({ + sourceRef: options.sourceRef, + targetRef: `refs/for/${options.targetBranch}`, + files: options.files, + pushOptions, + }); +} + export class GerritScm extends DefaultGitScm { override async branchExists(branchName: string): Promise { const searchConfig: GerritFindPRConfig = { @@ -144,21 +170,16 @@ export class GerritScm extends DefaultGitScm { const fetchRefSpec = currentRevision.ref; await git.fetchRevSpec(fetchRefSpec); // fetch current ChangeSet for git diff hasChanges = await git.hasDiff('HEAD', 'FETCH_HEAD'); // avoid pushing empty patch sets - // Only push to refs/for/ when updating an existing change if (hasChanges || commit.force) { - const pushOptions = ['notify=NONE']; - if (commit.autoApprove) { - pushOptions.push('label=Code-Review+2'); - } // Since the change already exists, we push to the same target branch to // avoid creating a new change if the base branch has changed. // updatePr() will later take care of moving the existing change to a // different base branch if needed. - const pushResult = await git.pushCommit({ + const pushResult = await pushForReview({ sourceRef: commit.branchName, - targetRef: `refs/for/${existingChange.branch}`, + targetBranch: existingChange.branch, files: commit.files, - pushOptions, + autoApprove: commit.autoApprove, }); /* v8 ignore else -- should never happen */ if (pushResult) { @@ -168,7 +189,7 @@ export class GerritScm extends DefaultGitScm { } else { // The push will be done by createPr() to actually create the Gerrit change logger.debug( - `Commit prepared for new change on branch ${commit.branchName}, but not pushed to refs/for/ yet`, + `Commit prepared but not pushed for review yet (${commit.baseBranch})`, ); return commitSha; } From 7106a0c4f145065ab8bc56a7baef76e5ae5b423b Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 3 Mar 2026 22:54:20 -0300 Subject: [PATCH 14/35] Fix after #41382 --- .../repository/onboarding/branch/create.spec.ts | 9 ++++++++- .../repository/onboarding/branch/create.ts | 2 +- .../repository/onboarding/branch/rebase.spec.ts | 15 ++++++++++++--- .../repository/onboarding/branch/rebase.ts | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/workers/repository/onboarding/branch/create.spec.ts b/lib/workers/repository/onboarding/branch/create.spec.ts index 328e84ce9ef..dd345903a09 100644 --- a/lib/workers/repository/onboarding/branch/create.spec.ts +++ b/lib/workers/repository/onboarding/branch/create.spec.ts @@ -19,6 +19,7 @@ describe('workers/repository/onboarding/branch/create', () => { GlobalConfig.set({ onboardingBranch: config.onboardingBranch, onboardingConfigFileName: config.onboardingConfigFileName, + onboardingPrTitle: 'Configure Renovate', }); }); @@ -231,7 +232,10 @@ describe('workers/repository/onboarding/branch/create', () => { const message = `${prefix}: add renovate.json`; config.semanticCommits = 'enabled'; - GlobalConfig.set({ onboardingBranch: config.onboardingBranch }); + GlobalConfig.set({ + onboardingBranch: config.onboardingBranch, + onboardingPrTitle: 'Configure Renovate', + }); await createOnboardingBranch(config); @@ -259,6 +263,7 @@ describe('workers/repository/onboarding/branch/create', () => { GlobalConfig.set({ onboardingBranch: config.onboardingBranch, onboardingConfigFileName: 'superConfigFile.yaml', + onboardingPrTitle: 'Configure Renovate', }); await createOnboardingBranch(config); @@ -288,6 +293,7 @@ describe('workers/repository/onboarding/branch/create', () => { GlobalConfig.set({ onboardingBranch: config.onboardingBranch, onboardingConfigFileName: path, + onboardingPrTitle: 'Configure Renovate', }); await createOnboardingBranch(config); @@ -317,6 +323,7 @@ describe('workers/repository/onboarding/branch/create', () => { GlobalConfig.set({ onboardingBranch: config.onboardingBranch, onboardingConfigFileName: path, + onboardingPrTitle: 'Configure Renovate', }); await createOnboardingBranch(config); diff --git a/lib/workers/repository/onboarding/branch/create.ts b/lib/workers/repository/onboarding/branch/create.ts index 7e41237ad70..981eb7a882a 100644 --- a/lib/workers/repository/onboarding/branch/create.ts +++ b/lib/workers/repository/onboarding/branch/create.ts @@ -45,7 +45,7 @@ export async function createOnboardingBranch( const prTitle = config.semanticCommits === 'enabled' ? getSemanticCommitPrTitle(config) - : config.onboardingPrTitle!; + : getInheritedOrGlobal('onboardingPrTitle')!; return scm.commitAndPush({ baseBranch: config.baseBranch, diff --git a/lib/workers/repository/onboarding/branch/rebase.spec.ts b/lib/workers/repository/onboarding/branch/rebase.spec.ts index 31464216921..4fe56d576fb 100644 --- a/lib/workers/repository/onboarding/branch/rebase.spec.ts +++ b/lib/workers/repository/onboarding/branch/rebase.spec.ts @@ -20,6 +20,7 @@ describe('workers/repository/onboarding/branch/rebase', () => { GlobalConfig.set({ localDir: '', onboardingConfigFileName: 'renovate.json', + onboardingPrTitle: 'Configure Renovate', platform: 'github', }); memCache.init(); @@ -32,7 +33,6 @@ describe('workers/repository/onboarding/branch/rebase', () => { onboardingConfig: { $schema: 'https://docs.renovatebot.com/renovate-schema.json', }, - onboardingPrTitle: 'Configure Renovate', repository: 'some/repo', }; configModule.getOnboardingConfigContents.mockResolvedValue(''); @@ -57,6 +57,7 @@ describe('workers/repository/onboarding/branch/rebase', () => { GlobalConfig.set({ localDir: '', onboardingConfigFileName: '.github/renovate.json', + onboardingPrTitle: 'Configure Renovate', platform: 'github', }); await rebaseOnboardingBranch(config, hash); @@ -73,7 +74,11 @@ describe('workers/repository/onboarding/branch/rebase', () => { }); it('falls back to "renovate.json" if onboardingConfigFileName is not set', async () => { - GlobalConfig.set({ localDir: '', platform: 'github' }); + GlobalConfig.set({ + localDir: '', + onboardingPrTitle: 'Configure Renovate', + platform: 'github', + }); await rebaseOnboardingBranch(config, hash); expect(scm.commitAndPush).toHaveBeenCalledTimes(1); expect(scm.commitAndPush.mock.calls[0][0].message).toContain( @@ -113,7 +118,11 @@ describe('workers/repository/onboarding/branch/rebase', () => { }); it('uses semantic commit PR title when semanticCommits is enabled', async () => { - GlobalConfig.set({ localDir: '', platform: 'github' }); + GlobalConfig.set({ + localDir: '', + onboardingPrTitle: 'Configure Renovate', + platform: 'github', + }); await rebaseOnboardingBranch( { ...config, diff --git a/lib/workers/repository/onboarding/branch/rebase.ts b/lib/workers/repository/onboarding/branch/rebase.ts index 3d774f3d545..a8aa8bb06ac 100644 --- a/lib/workers/repository/onboarding/branch/rebase.ts +++ b/lib/workers/repository/onboarding/branch/rebase.ts @@ -53,7 +53,7 @@ export async function rebaseOnboardingBranch( const prTitle = config.semanticCommits === 'enabled' ? getSemanticCommitPrTitle(config) - : config.onboardingPrTitle!; + : getInheritedOrGlobal('onboardingPrTitle')!; // TODO #22198 return scm.commitAndPush({ From c1b116868be9510b8e5d24085ee6b1876f397a41 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 4 Mar 2026 00:07:35 -0300 Subject: [PATCH 15/35] Fix onboarding --- lib/modules/platform/gerrit/scm.spec.ts | 32 +++++++++++++++++++++++++ lib/modules/platform/gerrit/scm.ts | 19 +++++++++++++++ lib/util/git/index.spec.ts | 26 ++++++++++++++++++++ lib/util/git/index.ts | 13 +++++++--- 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index bab443e7c1e..72d3bf19dbd 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -295,6 +295,38 @@ describe('modules/platform/gerrit/scm', () => { ); }); + it('unpushed branch uses local merge', async () => { + // Simulate commitAndPush for a new change (no existing Gerrit change) + clientMock.getBranchChange.mockResolvedValueOnce(null); + git.prepareCommit.mockResolvedValueOnce({ + commitSha: 'commitSha' as LongCommitSha, + parentCommitSha: 'parentSha' as LongCommitSha, + files: [], + }); + + // First, create the unpushed commit + await gerritScm.commitAndPush({ + branchName: 'renovate/onboarding', + baseBranch: 'main', + message: 'commit msg', + files: [], + prTitle: 'Configure Renovate', + }); + + // Now mergeToLocal should use the local merge instead of fetching + git.mergeToLocal.mockResolvedValueOnce(); + + await expect(gerritScm.mergeToLocal('renovate/onboarding')).toResolve(); + + // Should NOT query Gerrit API since we know the branch is unpushed + expect(clientMock.findChanges).not.toHaveBeenCalled(); + // Should use mergeToLocal with localBranch option + expect(git.mergeToLocal).toHaveBeenCalledExactlyOnceWith( + 'renovate/onboarding', + { localBranch: true }, + ); + }); + it('change exists', async () => { const change = partial({ current_revision: 'curSha', diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index ff623962623..f92b58c4a58 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -46,6 +46,13 @@ export async function pushForReview(options: { } export class GerritScm extends DefaultGitScm { + /** + * Branches that have a local commit prepared but no Gerrit change created yet + * (push is deferred to createPr()). These need special handling in + * mergeToLocal since they can't be fetched from origin. + */ + private pendingChangeBranches = new Set(); + override async branchExists(branchName: string): Promise { const searchConfig: GerritFindPRConfig = { state: 'open', @@ -206,6 +213,7 @@ export class GerritScm extends DefaultGitScm { }); /* v8 ignore else -- should never happen */ if (pushResult) { + this.pendingChangeBranches.delete(commit.branchName); return commitSha; } } @@ -214,6 +222,7 @@ export class GerritScm extends DefaultGitScm { logger.debug( `Commit prepared but not pushed for review yet (${commit.baseBranch})`, ); + this.pendingChangeBranches.add(commit.branchName); return commitSha; } } @@ -221,10 +230,20 @@ export class GerritScm extends DefaultGitScm { } override deleteBranch(branchName: string): Promise { + this.pendingChangeBranches.delete(branchName); return Promise.resolve(); } override async mergeToLocal(branchName: string): Promise { + // If the branch was committed locally but not yet pushed (deferred to + // createPr), we can't fetch it from origin. Merge the local branch directly. + if (this.pendingChangeBranches.has(branchName)) { + logger.debug( + `Branch ${branchName} has no Gerrit change yet, merging local branch`, + ); + return git.mergeToLocal(branchName, { localBranch: true }); + } + const searchConfig: GerritFindPRConfig = { state: 'open', branchName, diff --git a/lib/util/git/index.spec.ts b/lib/util/git/index.spec.ts index bc5bd9413fa..3b9af223dc2 100644 --- a/lib/util/git/index.spec.ts +++ b/lib/util/git/index.spec.ts @@ -554,6 +554,32 @@ describe('util/git/index', { timeout: 10000 }, () => { expect(pushSpy).toHaveBeenCalledTimes(0); }); + it('should merge a local-only branch without fetching from origin', async () => { + // Create a local-only branch (never pushed to origin) + await git.prepareCommit({ + branchName: 'renovate/local_only_branch', + message: 'local only commit', + files: [ + { type: 'addition', path: 'local_only_file', contents: 'local' }, + ], + }); + // Reset working tree back to default branch so the file is not present yet + const local = simpleGit(tmpDir.path); + await local.checkout(defaultBranch); + + expect(fs.existsSync(`${tmpDir.path}/local_only_file`)).toBeFalse(); + const fetchSpy = vi.spyOn(SimpleGit.prototype, 'fetch'); + const pushSpy = vi.spyOn(SimpleGit.prototype, 'push'); + + await git.mergeToLocal('renovate/local_only_branch', { + localBranch: true, + }); + + expect(fs.existsSync(`${tmpDir.path}/local_only_file`)).toBeTrue(); + expect(fetchSpy).toHaveBeenCalledTimes(0); + expect(pushSpy).toHaveBeenCalledTimes(0); + }); + it('should throw', async () => { await expect(git.mergeToLocal('not_found')).rejects.toThrow(); }); diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts index 2d63ad094b7..d18599b8d29 100644 --- a/lib/util/git/index.ts +++ b/lib/util/git/index.ts @@ -980,7 +980,10 @@ export async function deleteBranch(branchName: string): Promise { delete config.branchCommits[branchName]; } -export async function mergeToLocal(refSpecToMerge: string): Promise { +export async function mergeToLocal( + refSpecToMerge: string, + options?: { localBranch?: boolean }, +): Promise { let status: StatusResult | undefined; try { await syncGit(); @@ -994,8 +997,12 @@ export async function mergeToLocal(refSpecToMerge: string): Promise { ]), ); status = await git.status(); - await fetchRevSpec(refSpecToMerge); - await gitRetry(() => git.merge(['FETCH_HEAD'])); + if (options?.localBranch) { + await git.merge([refSpecToMerge]); + } else { + await fetchRevSpec(refSpecToMerge); + await git.merge(['FETCH_HEAD']); + } } catch (err) { logger.debug( { From 0d8fddeb3dac9edfb85a772734c8856a8b086ff0 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 4 Mar 2026 00:28:30 -0300 Subject: [PATCH 16/35] Cleanup code --- lib/modules/platform/gerrit/client.ts | 14 ++++++-------- lib/modules/platform/gerrit/index.ts | 7 ------- lib/modules/platform/gerrit/scm.ts | 20 +++++--------------- 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index 0e7d909463a..dc5400839dc 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -247,15 +247,13 @@ class GerritClient { changeNumber: number, hashtagsInput: GerritHashtagsInput, ): Promise { - if (hashtagsInput.add?.length === 0) { - delete hashtagsInput.add; - } - if (hashtagsInput.remove?.length === 0) { - delete hashtagsInput.remove; - } - if (hashtagsInput.add || hashtagsInput.remove) { + const add = hashtagsInput.add?.length ? hashtagsInput.add : undefined; + const remove = hashtagsInput.remove?.length + ? hashtagsInput.remove + : undefined; + if (add ?? remove) { await this.gerritHttp.postJson(`a/changes/${changeNumber}/hashtags`, { - body: hashtagsInput, + body: { add, remove }, }); } } diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index 0adab213bea..f3e78871704 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -202,12 +202,6 @@ export async function getPr(number: number): Promise { export async function updatePr(prConfig: UpdatePrConfig): Promise { logger.debug(`updatePr(${prConfig.number}, ${prConfig.prTitle})`); - /* v8 ignore next -- should never happen */ - if (prConfig.prTitle) { - logger.warn( - 'updatePr() called with prTitle - this should never happen as the title should be set via commitAndPush(), please report this issue.', - ); - } // prConfig.prBody will only be set if the body has changed if (prConfig.prBody) { await client.addMessage( @@ -254,7 +248,6 @@ export async function createPr(prConfig: CreatePRConfig): Promise { ); } - // Now find the newly created change const change = ( await client.findChanges(config.repository!, { branchName: prConfig.sourceBranch, diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index f92b58c4a58..c885fca8e6b 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -37,7 +37,7 @@ export async function pushForReview(options: { } } - return await git.pushCommit({ + return git.pushCommit({ sourceRef: options.sourceRef, targetRef: `refs/for/${options.targetBranch}`, files: options.files, @@ -46,11 +46,7 @@ export async function pushForReview(options: { } export class GerritScm extends DefaultGitScm { - /** - * Branches that have a local commit prepared but no Gerrit change created yet - * (push is deferred to createPr()). These need special handling in - * mergeToLocal since they can't be fetched from origin. - */ + /** Branches with a local commit but no Gerrit change yet (push deferred to createPr()). */ private pendingChangeBranches = new Set(); override async branchExists(branchName: string): Promise { @@ -218,10 +214,7 @@ export class GerritScm extends DefaultGitScm { } } } else { - // The push will be done by createPr() to actually create the Gerrit change - logger.debug( - `Commit prepared but not pushed for review yet (${commit.baseBranch})`, - ); + logger.debug(`Commit prepared, push deferred to createPr()`); this.pendingChangeBranches.add(commit.branchName); return commitSha; } @@ -235,12 +228,9 @@ export class GerritScm extends DefaultGitScm { } override async mergeToLocal(branchName: string): Promise { - // If the branch was committed locally but not yet pushed (deferred to - // createPr), we can't fetch it from origin. Merge the local branch directly. + // Unpushed branches can't be fetched from origin, merge locally instead if (this.pendingChangeBranches.has(branchName)) { - logger.debug( - `Branch ${branchName} has no Gerrit change yet, merging local branch`, - ); + logger.debug(`Merging local branch ${branchName} (not yet pushed)`); return git.mergeToLocal(branchName, { localBranch: true }); } From d2da9c157d671e0bf50bf3a8c27b843cd8fa7e9a Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 4 Mar 2026 01:11:20 -0300 Subject: [PATCH 17/35] Fix ESLint --- lib/modules/platform/gerrit/scm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index c885fca8e6b..933edc7e015 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -20,7 +20,7 @@ export function configureScm(repo: string, login: string): void { username = login; } -export async function pushForReview(options: { +export function pushForReview(options: { sourceRef: string; targetBranch: string; files: FileChange[]; From 787c76f20ceaabdeced450be81a90a6cefd2ac2c Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 24 Mar 2026 20:27:32 -0300 Subject: [PATCH 18/35] Fix tests --- lib/workers/repository/onboarding/branch/create.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/workers/repository/onboarding/branch/create.spec.ts b/lib/workers/repository/onboarding/branch/create.spec.ts index 85cc658b204..46c01319589 100644 --- a/lib/workers/repository/onboarding/branch/create.spec.ts +++ b/lib/workers/repository/onboarding/branch/create.spec.ts @@ -49,6 +49,7 @@ describe('workers/repository/onboarding/branch/create', () => { GlobalConfig.set({ onboardingCommitMessage: message, onboardingBranch: config.onboardingBranch, + onboardingPrTitle: 'Configure Renovate', }); await createOnboardingBranch(config); @@ -98,6 +99,7 @@ describe('workers/repository/onboarding/branch/create', () => { GlobalConfig.set({ onboardingCommitMessage: message, onboardingBranch: config.onboardingBranch, + onboardingPrTitle: 'Configure Renovate', }); await createOnboardingBranch({ @@ -159,6 +161,7 @@ describe('workers/repository/onboarding/branch/create', () => { GlobalConfig.set({ onboardingCommitMessage: text, onboardingBranch: config.onboardingBranch, + onboardingPrTitle: 'Configure Renovate', }); await createOnboardingBranch(config); @@ -217,6 +220,7 @@ describe('workers/repository/onboarding/branch/create', () => { GlobalConfig.set({ onboardingCommitMessage: text, onboardingBranch: config.onboardingBranch, + onboardingPrTitle: 'Configure Renovate', }); await createOnboardingBranch(config); From eab8b33dcf1c1423f9bed5fd86ab28495c22c678 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 27 Mar 2026 12:40:59 -0300 Subject: [PATCH 19/35] Restore gitRetry for git.merge --- lib/util/git/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts index d18599b8d29..3f4fddee346 100644 --- a/lib/util/git/index.ts +++ b/lib/util/git/index.ts @@ -1001,7 +1001,7 @@ export async function mergeToLocal( await git.merge([refSpecToMerge]); } else { await fetchRevSpec(refSpecToMerge); - await git.merge(['FETCH_HEAD']); + await gitRetry(() => git.merge(['FETCH_HEAD'])); } } catch (err) { logger.debug( From 206626c11f610e5a9ed9c80a8c07ac2994fd0595 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Fri, 27 Mar 2026 13:40:15 -0300 Subject: [PATCH 20/35] Fix tests --- lib/modules/platform/gerrit/index.spec.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/modules/platform/gerrit/index.spec.ts b/lib/modules/platform/gerrit/index.spec.ts index ab06a046d39..6cdbabd30f1 100644 --- a/lib/modules/platform/gerrit/index.spec.ts +++ b/lib/modules/platform/gerrit/index.spec.ts @@ -374,7 +374,7 @@ describe('modules/platform/gerrit/index', () => { sourceRef: 'source', targetRef: 'refs/for/target', files: [], - pushOptions: ['notify=NONE'], + pushOptions: ['notify=NONE', 'ready'], }); expect(clientMock.addMessage).toHaveBeenCalledExactlyOnceWith( 123456, @@ -410,7 +410,7 @@ describe('modules/platform/gerrit/index', () => { sourceRef: 'source', targetRef: 'refs/for/target', files: [], - pushOptions: ['notify=NONE', 'label=Code-Review+2'], + pushOptions: ['notify=NONE', 'ready', 'label=Code-Review+2'], }); expect(clientMock.addMessage).toHaveBeenCalledExactlyOnceWith( 123456, @@ -444,7 +444,12 @@ describe('modules/platform/gerrit/index', () => { sourceRef: 'source', targetRef: 'refs/for/target', files: [], - pushOptions: ['notify=NONE', 'hashtag=label1', 'hashtag=label2'], + pushOptions: [ + 'notify=NONE', + 'ready', + 'hashtag=label1', + 'hashtag=label2', + ], }); expect(clientMock.addMessage).toHaveBeenCalledExactlyOnceWith( 123456, From 9d195c89269bbf7b4a620f242619b6b47e86c9a6 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Sat, 28 Mar 2026 10:45:46 -0300 Subject: [PATCH 21/35] Simplify hashtags code --- lib/modules/platform/gerrit/client.ts | 8 +++----- lib/modules/platform/gerrit/index.ts | 10 ++++------ lib/modules/platform/gerrit/types.ts | 4 ++-- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/modules/platform/gerrit/client.ts b/lib/modules/platform/gerrit/client.ts index dc5400839dc..beff4739e1d 100644 --- a/lib/modules/platform/gerrit/client.ts +++ b/lib/modules/platform/gerrit/client.ts @@ -1,3 +1,4 @@ +import { isNonEmptyArray } from '@sindresorhus/is'; import semver from 'semver'; import { z } from 'zod/v3'; import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages.ts'; @@ -247,11 +248,8 @@ class GerritClient { changeNumber: number, hashtagsInput: GerritHashtagsInput, ): Promise { - const add = hashtagsInput.add?.length ? hashtagsInput.add : undefined; - const remove = hashtagsInput.remove?.length - ? hashtagsInput.remove - : undefined; - if (add ?? remove) { + const { add, remove } = hashtagsInput; + if (isNonEmptyArray(add) || isNonEmptyArray(remove)) { await this.gerritHttp.postJson(`a/changes/${changeNumber}/hashtags`, { body: { add, remove }, }); diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index f3e78871704..36f78587628 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -210,12 +210,10 @@ export async function updatePr(prConfig: UpdatePrConfig): Promise { TAG_PULL_REQUEST_BODY, ); } - if (prConfig.addLabels?.length || prConfig.removeLabels?.length) { - await client.setHashtags(prConfig.number, { - add: prConfig.addLabels ?? undefined, - remove: prConfig.removeLabels ?? undefined, - }); - } + await client.setHashtags(prConfig.number, { + add: prConfig.addLabels, + remove: prConfig.removeLabels, + }); if (prConfig.targetBranch) { await client.moveChange(prConfig.number, prConfig.targetBranch); } diff --git a/lib/modules/platform/gerrit/types.ts b/lib/modules/platform/gerrit/types.ts index bd4aca11d92..c7d7148607e 100644 --- a/lib/modules/platform/gerrit/types.ts +++ b/lib/modules/platform/gerrit/types.ts @@ -146,6 +146,6 @@ export interface GerritMergeableInfo { } export interface GerritHashtagsInput { - add?: string[]; - remove?: string[]; + add?: string[] | null; + remove?: string[] | null; } From f4d225568deb4ae9cd92b9fd9b557dd603f78532 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Sat, 28 Mar 2026 11:02:52 -0300 Subject: [PATCH 22/35] Delete pendingChangeBranch in pushForReview() --- lib/modules/platform/gerrit/scm.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index 59219471615..7e2b7d7b135 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -20,7 +20,10 @@ export function configureScm(repo: string, login: string): void { username = login; } -export function pushForReview(options: { +/** Branches with a local commit but no Gerrit change yet (push deferred to createPr()). */ +const pendingChangeBranches = new Set(); + +export async function pushForReview(options: { sourceRef: string; targetBranch: string; files: FileChange[]; @@ -37,18 +40,19 @@ export function pushForReview(options: { } } - return git.pushCommit({ + const result = await git.pushCommit({ sourceRef: options.sourceRef, targetRef: `refs/for/${options.targetBranch}`, files: options.files, pushOptions, }); + if (result) { + pendingChangeBranches.delete(options.sourceRef); + } + return result; } export class GerritScm extends DefaultGitScm { - /** Branches with a local commit but no Gerrit change yet (push deferred to createPr()). */ - private pendingChangeBranches = new Set(); - override async branchExists(branchName: string): Promise { const searchConfig: GerritFindPRConfig = { state: 'open', @@ -209,13 +213,12 @@ export class GerritScm extends DefaultGitScm { }); /* v8 ignore else -- should never happen */ if (pushResult) { - this.pendingChangeBranches.delete(commit.branchName); return commitSha; } } } else { logger.debug(`Commit prepared, push deferred to createPr()`); - this.pendingChangeBranches.add(commit.branchName); + pendingChangeBranches.add(commit.branchName); return commitSha; } } @@ -223,13 +226,13 @@ export class GerritScm extends DefaultGitScm { } override deleteBranch(branchName: string): Promise { - this.pendingChangeBranches.delete(branchName); + pendingChangeBranches.delete(branchName); return Promise.resolve(); } override async mergeToLocal(branchName: string): Promise { // Unpushed branches can't be fetched from origin, merge locally instead - if (this.pendingChangeBranches.has(branchName)) { + if (pendingChangeBranches.has(branchName)) { logger.debug(`Merging local branch ${branchName} (not yet pushed)`); return git.mergeToLocal(branchName, { localBranch: true }); } From 302a2e6ec0d5c56241beed91e24f6113952611ba Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 31 Mar 2026 19:05:38 -0300 Subject: [PATCH 23/35] Fix test name --- lib/modules/platform/gerrit/scm.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 2167aa6015d..31772383213 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -464,7 +464,7 @@ describe('modules/platform/gerrit/scm', () => { }); }); - it('commitFiles() - existing change-set without new changes', async () => { + it('commitAndPush() - existing change-set without new changes', async () => { const existingChange = partial({ change_id: 'I1bf983f8f6530c44826925b1308a45fe672408a6', branch: 'main', From 08054ca4acc97f02ca55a1c0ee36500d3c6dc455 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 31 Mar 2026 19:12:03 -0300 Subject: [PATCH 24/35] Remove unnecessary test comments --- lib/modules/platform/gerrit/scm.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 31772383213..3b253d0ad5e 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -303,8 +303,6 @@ describe('modules/platform/gerrit/scm', () => { parentCommitSha: 'parentSha' as LongCommitSha, files: [], }); - - // First, create the unpushed commit await gerritScm.commitAndPush({ branchName: 'renovate/onboarding', baseBranch: 'main', @@ -315,12 +313,9 @@ describe('modules/platform/gerrit/scm', () => { // Now mergeToLocal should use the local merge instead of fetching git.mergeToLocal.mockResolvedValueOnce(); - await expect(gerritScm.mergeToLocal('renovate/onboarding')).toResolve(); - // Should NOT query Gerrit API since we know the branch is unpushed expect(clientMock.findChanges).not.toHaveBeenCalled(); - // Should use mergeToLocal with localBranch option expect(git.mergeToLocal).toHaveBeenCalledExactlyOnceWith( 'renovate/onboarding', { localBranch: true }, From 0c06d4a8692cbdf5646a97f9f0c0239ebbde0361 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 31 Mar 2026 19:17:36 -0300 Subject: [PATCH 25/35] Improve test name again --- lib/modules/platform/gerrit/scm.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 3b253d0ad5e..3393b217628 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -295,8 +295,8 @@ describe('modules/platform/gerrit/scm', () => { ); }); - it('unpushed branch uses local merge', async () => { - // Simulate commitAndPush for a new change (no existing Gerrit change) + it('uses local merge when there is a pending change branch', async () => { + // Creates a pending change branch clientMock.getBranchChange.mockResolvedValueOnce(null); git.prepareCommit.mockResolvedValueOnce({ commitSha: 'commitSha' as LongCommitSha, @@ -311,10 +311,8 @@ describe('modules/platform/gerrit/scm', () => { prTitle: 'Configure Renovate', }); - // Now mergeToLocal should use the local merge instead of fetching git.mergeToLocal.mockResolvedValueOnce(); await expect(gerritScm.mergeToLocal('renovate/onboarding')).toResolve(); - // Should NOT query Gerrit API since we know the branch is unpushed expect(clientMock.findChanges).not.toHaveBeenCalled(); expect(git.mergeToLocal).toHaveBeenCalledExactlyOnceWith( 'renovate/onboarding', From 6cbcfefbe2dfe5e7061ecb7880905e19c79e13f0 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 31 Mar 2026 19:29:17 -0300 Subject: [PATCH 26/35] Fix another test name --- lib/modules/platform/gerrit/scm.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 3393b217628..7acd4c89269 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -457,7 +457,7 @@ describe('modules/platform/gerrit/scm', () => { }); }); - it('commitAndPush() - existing change-set without new changes', async () => { + it('commitAndPush() - existing change without new changes', async () => { const existingChange = partial({ change_id: 'I1bf983f8f6530c44826925b1308a45fe672408a6', branch: 'main', From baa924216a6fc959518ab5433b837cbc72ccecc2 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 31 Mar 2026 20:20:35 -0300 Subject: [PATCH 27/35] Delete local branch in Gerrit SCM --- lib/modules/platform/gerrit/scm.spec.ts | 3 +++ lib/modules/platform/gerrit/scm.ts | 4 +-- lib/util/git/index.spec.ts | 8 ++++++ lib/util/git/index.ts | 33 ++++++++++++++----------- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 7acd4c89269..08e734e23fa 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -272,6 +272,9 @@ describe('modules/platform/gerrit/scm', () => { it('deleteBranch()', async () => { await expect(gerritScm.deleteBranch('branchName')).toResolve(); + expect(git.deleteBranch).toHaveBeenCalledExactlyOnceWith('branchName', { + localBranch: true, + }); }); describe('mergeToLocal', () => { diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index 7e2b7d7b135..e283e51f863 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -225,9 +225,9 @@ export class GerritScm extends DefaultGitScm { return null; // empty commit, no changes in this Gerrit Change } - override deleteBranch(branchName: string): Promise { + override async deleteBranch(branchName: string): Promise { pendingChangeBranches.delete(branchName); - return Promise.resolve(); + await git.deleteBranch(branchName, { localBranch: true }); } override async mergeToLocal(branchName: string): Promise { diff --git a/lib/util/git/index.spec.ts b/lib/util/git/index.spec.ts index 3b9af223dc2..9fef0e9034d 100644 --- a/lib/util/git/index.spec.ts +++ b/lib/util/git/index.spec.ts @@ -615,6 +615,14 @@ describe('util/git/index', { timeout: 10000 }, () => { '--no-verify', ]); }); + + it('should only delete local branch when localBranch option is set', async () => { + const rawSpy = vi.spyOn(SimpleGit.prototype, 'raw'); + await git.deleteBranch('renovate/past_branch', { localBranch: true }); + expect(rawSpy).not.toHaveBeenCalledWith( + expect.arrayContaining(['push', '--delete']), + ); + }); }); describe('getBranchLastCommitTime', () => { diff --git a/lib/util/git/index.ts b/lib/util/git/index.ts index 3f4fddee346..4d468aa71e4 100644 --- a/lib/util/git/index.ts +++ b/lib/util/git/index.ts @@ -946,24 +946,29 @@ export async function isBranchConflicted( return result; } -export async function deleteBranch(branchName: string): Promise { +export async function deleteBranch( + branchName: string, + options?: { localBranch?: boolean }, +): Promise { await syncGit(); - try { - const deleteCommand = ['push', '--delete', 'origin', branchName]; + if (!options?.localBranch) { + try { + const deleteCommand = ['push', '--delete', 'origin', branchName]; - if (getNoVerify().includes('push')) { - deleteCommand.push('--no-verify'); - } + if (getNoVerify().includes('push')) { + deleteCommand.push('--no-verify'); + } - await gitRetry(() => git.raw(deleteCommand)); - logger.debug(`Deleted remote branch: ${branchName}`); - } catch (err) { - const errChecked = checkForPlatformFailure(err); - /* v8 ignore if -- TODO: add test #40625 */ - if (errChecked) { - throw errChecked; + await gitRetry(() => git.raw(deleteCommand)); + logger.debug(`Deleted remote branch: ${branchName}`); + } catch (err) { + const errChecked = checkForPlatformFailure(err); + /* v8 ignore if -- TODO: add test #40625 */ + if (errChecked) { + throw errChecked; + } + logger.debug(`No remote branch to delete with name: ${branchName}`); } - logger.debug(`No remote branch to delete with name: ${branchName}`); } try { await deleteLocalBranch(branchName); From af2bd21e4495a77a9c779cf334986c24b41adfee Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 31 Mar 2026 21:18:25 -0300 Subject: [PATCH 28/35] Add test for deleting pending branch --- lib/modules/platform/gerrit/scm.spec.ts | 35 +++++++++++-------------- lib/modules/platform/gerrit/scm.ts | 2 +- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 08e734e23fa..b3d37909e7b 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -2,7 +2,7 @@ import { DateTime } from 'luxon'; import { git, partial } from '~test/util.ts'; import type { LongCommitSha } from '../../../util/git/types.ts'; import { client as _client } from './client.ts'; -import { GerritScm, configureScm } from './scm.ts'; +import { GerritScm, configureScm, pendingChangeBranches } from './scm.ts'; import type { GerritAccountInfo, GerritChange, @@ -17,6 +17,7 @@ describe('modules/platform/gerrit/scm', () => { beforeEach(() => { configureScm('test/repo', 'user'); + pendingChangeBranches.clear(); }); describe('isBranchBehindBase()', () => { @@ -270,10 +271,18 @@ describe('modules/platform/gerrit/scm', () => { }); }); - it('deleteBranch()', async () => { - await expect(gerritScm.deleteBranch('branchName')).toResolve(); - expect(git.deleteBranch).toHaveBeenCalledExactlyOnceWith('branchName', { - localBranch: true, + describe('deleteBranch()', () => { + it('deletes local branch', async () => { + await expect(gerritScm.deleteBranch('branchName')).toResolve(); + expect(git.deleteBranch).toHaveBeenCalledExactlyOnceWith('branchName', { + localBranch: true, + }); + }); + + it('clears pending change branch', async () => { + pendingChangeBranches.add('renovate/pending'); + await gerritScm.deleteBranch('renovate/pending'); + expect(pendingChangeBranches.has('renovate/pending')).toBeFalse(); }); }); @@ -299,21 +308,7 @@ describe('modules/platform/gerrit/scm', () => { }); it('uses local merge when there is a pending change branch', async () => { - // Creates a pending change branch - clientMock.getBranchChange.mockResolvedValueOnce(null); - git.prepareCommit.mockResolvedValueOnce({ - commitSha: 'commitSha' as LongCommitSha, - parentCommitSha: 'parentSha' as LongCommitSha, - files: [], - }); - await gerritScm.commitAndPush({ - branchName: 'renovate/onboarding', - baseBranch: 'main', - message: 'commit msg', - files: [], - prTitle: 'Configure Renovate', - }); - + pendingChangeBranches.add('renovate/onboarding'); git.mergeToLocal.mockResolvedValueOnce(); await expect(gerritScm.mergeToLocal('renovate/onboarding')).toResolve(); expect(clientMock.findChanges).not.toHaveBeenCalled(); diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index e283e51f863..7abcbd7c790 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -21,7 +21,7 @@ export function configureScm(repo: string, login: string): void { } /** Branches with a local commit but no Gerrit change yet (push deferred to createPr()). */ -const pendingChangeBranches = new Set(); +export const pendingChangeBranches = new Set(); export async function pushForReview(options: { sourceRef: string; From a97c8d7a4e4c703ea3987ebb77dc75a1d4dce901 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 31 Mar 2026 21:34:12 -0300 Subject: [PATCH 29/35] Add tests for pushForReview() --- lib/modules/platform/gerrit/scm.spec.ts | 75 ++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index b3d37909e7b..ebb56a0c653 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -2,7 +2,12 @@ import { DateTime } from 'luxon'; import { git, partial } from '~test/util.ts'; import type { LongCommitSha } from '../../../util/git/types.ts'; import { client as _client } from './client.ts'; -import { GerritScm, configureScm, pendingChangeBranches } from './scm.ts'; +import { + GerritScm, + configureScm, + pendingChangeBranches, + pushForReview, +} from './scm.ts'; import type { GerritAccountInfo, GerritChange, @@ -271,6 +276,74 @@ describe('modules/platform/gerrit/scm', () => { }); }); + describe('pushForReview()', () => { + it('pushes to refs/for/ and returns true on success', async () => { + git.pushCommit.mockResolvedValueOnce(true); + await expect( + pushForReview({ + sourceRef: 'renovate/feat', + targetBranch: 'main', + files: [], + }), + ).resolves.toBeTrue(); + expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ + sourceRef: 'renovate/feat', + targetRef: 'refs/for/main', + files: [], + pushOptions: ['notify=NONE', 'ready'], + }); + }); + + it('adds hashtag push options for each label', async () => { + git.pushCommit.mockResolvedValueOnce(true); + await expect( + pushForReview({ + sourceRef: 'renovate/feat', + targetBranch: 'main', + files: [], + labels: ['team:backend', 'priority:high'], + }), + ).resolves.toBeTrue(); + expect(git.pushCommit).toHaveBeenCalledExactlyOnceWith({ + sourceRef: 'renovate/feat', + targetRef: 'refs/for/main', + files: [], + pushOptions: [ + 'notify=NONE', + 'ready', + 'hashtag=team:backend', + 'hashtag=priority:high', + ], + }); + }); + + it('clears pending change branch on success', async () => { + pendingChangeBranches.add('renovate/feat'); + git.pushCommit.mockResolvedValueOnce(true); + await expect( + pushForReview({ + sourceRef: 'renovate/feat', + targetBranch: 'main', + files: [], + }), + ).resolves.toBeTrue(); + expect(pendingChangeBranches.has('renovate/feat')).toBeFalse(); + }); + + it('keeps pending change branch when push fails', async () => { + pendingChangeBranches.add('renovate/feat'); + git.pushCommit.mockResolvedValueOnce(false); + await expect( + pushForReview({ + sourceRef: 'renovate/feat', + targetBranch: 'main', + files: [], + }), + ).resolves.toBeFalse(); + expect(pendingChangeBranches.has('renovate/feat')).toBeTrue(); + }); + }); + describe('deleteBranch()', () => { it('deletes local branch', async () => { await expect(gerritScm.deleteBranch('branchName')).toResolve(); From 6595300f36a520b9cf437c36fa4f868aba9c7ecc Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 1 Apr 2026 20:07:16 +0200 Subject: [PATCH 30/35] Fix test case name --- lib/modules/platform/gerrit/scm.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index ebb56a0c653..e303d7567b8 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -479,7 +479,7 @@ describe('modules/platform/gerrit/scm', () => { expect(git.pushCommit).not.toHaveBeenCalled(); }); - it('commitAndPush() - existing change without new changes', async () => { + it('commitAndPush() - existing change keeps original target branch', async () => { const existingChange = partial({ change_id: 'Ifcd936eef0ced620040a07a337c586d0a882725b', branch: 'main', From 5023ffba6e5365b9ecc9313921c7c8b5324de831 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 1 Apr 2026 20:14:30 +0200 Subject: [PATCH 31/35] Improve another test name --- lib/modules/platform/gerrit/scm.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index e303d7567b8..014ee9bf3a0 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -445,7 +445,7 @@ describe('modules/platform/gerrit/scm', () => { ); }); - it('commitAndPush() - create first commit', async () => { + it('commitAndPush() - create first commit but does not push', async () => { clientMock.findChanges.mockResolvedValueOnce([]); git.prepareCommit.mockResolvedValueOnce({ commitSha: 'commitSha' as LongCommitSha, From 9517bc2182d1f7475892ec2e57e8f84a52c99430 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 1 Apr 2026 20:55:42 +0200 Subject: [PATCH 32/35] Fix another small test inconsistency --- lib/modules/platform/gerrit/scm.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index 014ee9bf3a0..c68eaefffb8 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -359,7 +359,7 @@ describe('modules/platform/gerrit/scm', () => { }); }); - describe('mergeToLocal', () => { + describe('mergeToLocal()', () => { it('no change exists', async () => { clientMock.findChanges.mockResolvedValueOnce([]); git.mergeToLocal.mockResolvedValueOnce(); @@ -446,7 +446,7 @@ describe('modules/platform/gerrit/scm', () => { }); it('commitAndPush() - create first commit but does not push', async () => { - clientMock.findChanges.mockResolvedValueOnce([]); + clientMock.getBranchChange.mockResolvedValueOnce(null); git.prepareCommit.mockResolvedValueOnce({ commitSha: 'commitSha' as LongCommitSha, parentCommitSha: 'parentSha' as LongCommitSha, From 2f44600c750e52cd308bcd667fe4f0b93f19de85 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 15 Apr 2026 19:36:39 -0300 Subject: [PATCH 33/35] Fix some code style issues --- lib/modules/platform/gerrit/index.spec.ts | 1 - lib/modules/platform/gerrit/scm.ts | 3 ++- lib/util/git/index.spec.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/modules/platform/gerrit/index.spec.ts b/lib/modules/platform/gerrit/index.spec.ts index 6cdbabd30f1..a1bc6daad5b 100644 --- a/lib/modules/platform/gerrit/index.spec.ts +++ b/lib/modules/platform/gerrit/index.spec.ts @@ -891,7 +891,6 @@ describe('modules/platform/gerrit/index', () => { it('deleteLabel() - deletes a label', async () => { const pro = gerrit.deleteLabel(123456, 'hashtag1'); await expect(pro).resolves.toBeUndefined(); - expect(clientMock.setHashtags).toHaveBeenCalledTimes(1); expect(clientMock.setHashtags).toHaveBeenCalledExactlyOnceWith(123456, { remove: ['hashtag1'], }); diff --git a/lib/modules/platform/gerrit/scm.ts b/lib/modules/platform/gerrit/scm.ts index 7abcbd7c790..18f4512de5e 100644 --- a/lib/modules/platform/gerrit/scm.ts +++ b/lib/modules/platform/gerrit/scm.ts @@ -1,3 +1,4 @@ +import { isNonEmptyArray } from '@sindresorhus/is'; import { randomUUID } from 'crypto'; import { DateTime } from 'luxon'; import { logger } from '../../../logger/index.ts'; @@ -34,7 +35,7 @@ export async function pushForReview(options: { if (options.autoApprove) { pushOptions.push('label=Code-Review+2'); } - if (options.labels) { + if (isNonEmptyArray(options.labels)) { for (const label of options.labels) { pushOptions.push(`hashtag=${label}`); } diff --git a/lib/util/git/index.spec.ts b/lib/util/git/index.spec.ts index 0d1c39320b3..7f88731219c 100644 --- a/lib/util/git/index.spec.ts +++ b/lib/util/git/index.spec.ts @@ -576,8 +576,8 @@ describe('util/git/index', { timeout: 10000 }, () => { }); expect(fs.existsSync(`${tmpDir.path}/local_only_file`)).toBeTrue(); - expect(fetchSpy).toHaveBeenCalledTimes(0); - expect(pushSpy).toHaveBeenCalledTimes(0); + expect(fetchSpy).not.toHaveBeenCalled(); + expect(pushSpy).not.toHaveBeenCalled(); }); it('should throw', async () => { From 7cf1bb400ff92b13fe2c16525b661b215632aee5 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 15 Apr 2026 21:56:37 -0300 Subject: [PATCH 34/35] Use isUndefined instead --- 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 36f78587628..dabbb0fb1d8 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -1,4 +1,4 @@ -import { isTruthy } from '@sindresorhus/is'; +import { isTruthy, isUndefined } from '@sindresorhus/is'; import semver from 'semver'; import { logger } from '../../../logger/index.ts'; import type { BranchStatus } from '../../../types/index.ts'; @@ -255,7 +255,7 @@ export async function createPr(prConfig: CreatePRConfig): Promise { requestDetails: REQUEST_DETAILS_FOR_PRS, }) ).pop(); - if (change === undefined) { + if (isUndefined(change)) { throw new Error( `Could not find the Gerrit change after pushing to refs/for/${prConfig.targetBranch}`, ); From 0a7ea9b7e3f217e3242df3455355f88c4e9f4b70 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Wed, 15 Apr 2026 22:02:29 -0300 Subject: [PATCH 35/35] Fix one small test style issue --- lib/modules/platform/gerrit/scm.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/platform/gerrit/scm.spec.ts b/lib/modules/platform/gerrit/scm.spec.ts index c68eaefffb8..da0eea405fd 100644 --- a/lib/modules/platform/gerrit/scm.spec.ts +++ b/lib/modules/platform/gerrit/scm.spec.ts @@ -569,7 +569,7 @@ describe('modules/platform/gerrit/scm', () => { expect(git.fetchRevSpec).toHaveBeenCalledExactlyOnceWith( 'refs/changes/1/2', ); - expect(git.pushCommit).toHaveBeenCalledTimes(0); + expect(git.pushCommit).not.toHaveBeenCalled(); }); it('commitAndPush() - existing change with new changes - auto-approve', async () => {