Skip to content
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
69d72c2
fix(gerrit): defer change creation to `createPr()`
felipecrs Nov 14, 2025
0b491aa
Refactor hashtags handling
felipecrs Nov 14, 2025
8d3d5a7
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Dec 14, 2025
e512e13
Add prTitle to all missing commitAndPush()
felipecrs Dec 14, 2025
0240825
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Jan 30, 2026
676016a
Fix gerrit tests
felipecrs Jan 30, 2026
1ddaf28
Add warning when prTitle is set in updatePr
felipecrs Jan 30, 2026
8d9f0c2
Remove now useless labels from commitAndPush
felipecrs Jan 30, 2026
a64c2b3
Fix gerrit client coverage
felipecrs Jan 30, 2026
5acc7f0
Fix gerrit scm coverage
felipecrs Jan 30, 2026
11009d9
Fix rebase coverage
felipecrs Jan 31, 2026
2c25eb2
Improve TODO about assignees
felipecrs Feb 1, 2026
21887db
Merge branch 'main' into gerrit-fix-create-pr
felipecrs Feb 2, 2026
082468c
Fix formatting
felipecrs Feb 2, 2026
ea9d902
Fix review comment
felipecrs Feb 3, 2026
3f7840d
Merge branch 'main' into gerrit-fix-create-pr
felipecrs Feb 3, 2026
00b8f16
Extract to a pushForReview function
felipecrs Feb 3, 2026
15022ab
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Feb 6, 2026
d804361
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Feb 20, 2026
32364b6
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Mar 4, 2026
7106a0c
Fix after #41382
felipecrs Mar 4, 2026
c1b1168
Fix onboarding
felipecrs Mar 4, 2026
0d8fdde
Cleanup code
felipecrs Mar 4, 2026
d2da9c1
Fix ESLint
felipecrs Mar 4, 2026
3a056ae
Merge branch 'main' into gerrit-fix-create-pr
felipecrs Mar 24, 2026
787c76f
Fix tests
felipecrs Mar 24, 2026
352d9f3
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Mar 27, 2026
eab8b33
Restore gitRetry for git.merge
felipecrs Mar 27, 2026
206626c
Fix tests
felipecrs Mar 27, 2026
9d195c8
Simplify hashtags code
felipecrs Mar 28, 2026
f4d2255
Delete pendingChangeBranch in pushForReview()
felipecrs Mar 28, 2026
d040452
Merge branch 'main' into gerrit-fix-create-pr
felipecrs Mar 28, 2026
5c453a0
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Mar 31, 2026
81ff09c
Merge branch 'main' into gerrit-fix-create-pr
felipecrs Mar 31, 2026
302a2e6
Fix test name
felipecrs Mar 31, 2026
08054ca
Remove unnecessary test comments
felipecrs Mar 31, 2026
0c06d4a
Improve test name again
felipecrs Mar 31, 2026
6cbcfef
Fix another test name
felipecrs Mar 31, 2026
baa9242
Delete local branch in Gerrit SCM
felipecrs Mar 31, 2026
af2bd21
Add test for deleting pending branch
felipecrs Apr 1, 2026
a97c8d7
Add tests for pushForReview()
felipecrs Apr 1, 2026
a261b32
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Apr 1, 2026
6595300
Fix test case name
felipecrs Apr 1, 2026
5023ffb
Improve another test name
felipecrs Apr 1, 2026
9517bc2
Fix another small test inconsistency
felipecrs Apr 1, 2026
163e5e0
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Apr 1, 2026
ffbe8a1
Merge branch 'main' into gerrit-fix-create-pr
felipecrs Apr 8, 2026
2f44600
Fix some code style issues
felipecrs Apr 15, 2026
c0a8dd4
Merge branch 'main' of https://github.com/felipecrs/renovate into ger…
felipecrs Apr 15, 2026
45b1014
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Apr 15, 2026
7cf1bb4
Use isUndefined instead
felipecrs Apr 16, 2026
0a7ea9b
Fix one small test style issue
felipecrs Apr 16, 2026
50359c1
Merge branch 'main' of https://github.com/renovatebot/renovate into g…
felipecrs Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions lib/modules/platform/gerrit/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,15 +715,54 @@ 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();
await expect(client.setHashtags(123456, { add: [] })).toResolve();
await expect(client.setHashtags(123456, { remove: [] })).toResolve();
await expect(
client.setHashtags(123456, { add: [], remove: [] }),
).toResolve();
});
});

Expand Down
18 changes: 13 additions & 5 deletions lib/modules/platform/gerrit/client.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,6 +12,7 @@ import type {
GerritChange,
GerritChangeMessageInfo,
GerritFindPRConfig,
GerritHashtagsInput,
GerritMergeableInfo,
GerritProjectInfo,
GerritRequestDetail,
Expand Down Expand Up @@ -242,10 +244,16 @@ class GerritClient {
);
}

async deleteHashtag(changeNumber: number, hashtag: string): Promise<void> {
await this.gerritHttp.postJson(`a/changes/${changeNumber}/hashtags`, {
body: { remove: [hashtag] },
});
async setHashtags(
changeNumber: number,
hashtagsInput: GerritHashtagsInput,
): Promise<void> {
const { add, remove } = hashtagsInput;
if (isNonEmptyArray(add) || isNonEmptyArray(remove)) {
await this.gerritHttp.postJson(`a/changes/${changeNumber}/hashtags`, {
body: { add, remove },
});
}
}

async addReviewers(changeNumber: number, reviewers: string[]): Promise<void> {
Expand All @@ -262,7 +270,7 @@ class GerritClient {

async addAssignee(changeNumber: number, assignee: string): Promise<void> {
await this.gerritHttp.putJson<GerritAccountInfo>(
// TODO: refactor this as this API removed in Gerrit 3.8
// TODO: use CCs instead as Gerrit 3.8+ no longer supports assignees
Comment thread
felipecrs marked this conversation as resolved.
`a/changes/${changeNumber}/assignee`,
{
body: { assignee },
Expand Down
186 changes: 143 additions & 43 deletions lib/modules/platform/gerrit/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { codeBlock } from 'common-tags';
import { DateTime } from 'luxon';
import { git, hostRules, partial } from '~test/util.ts';
import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages.ts';
import type { BranchStatus } from '../../../types/index.ts';
Expand Down Expand Up @@ -39,15 +38,6 @@ vi.mock('./client.ts');
const clientMock = vi.mocked(_client);

describe('modules/platform/gerrit/index', () => {
const t0 = DateTime.fromISO(
'2025-04-14T16:33:37.000000000',
).toUTC() as DateTime<true>;

beforeAll(() => {
vi.useFakeTimers();
vi.setSystemTime(t0.toMillis());
});

beforeEach(async () => {
hostRules.find.mockReturnValue({
username: 'user',
Expand Down Expand Up @@ -299,6 +289,47 @@ describe('modules/platform/gerrit/index', () => {
TAG_PULL_REQUEST_BODY,
);
});

it('updatePr() - with addLabels => add hashtags', async () => {
const change = partial<GerritChange>({});
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<GerritChange>({});
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<GerritChange>({});
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'],
});
});
});

it('updatePr() - targetBranch set => move the change', async () => {
Expand All @@ -319,71 +350,142 @@ 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<GerritChange>({
_number: 123456,
current_revision: 'some-revision',
revisions: {
'some-revision': partial<GerritRevisionInfo>({
commit_with_footers: 'Renovate-Branch: source',
}),
},
created: '2025-04-14 16:33:37.000000000',
});
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', 'ready'],
});
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<GerritChange>({
_number: 123456,
created: t0.minus({ minutes: 6 }).toISO().replace('T', ' '),
current_revision: 'some-revision',
revisions: {
'some-revision': partial<GerritRevisionInfo>({
commit_with_footers: 'Renovate-Branch: source',
}),
},
created: '2025-04-14 16:33:37.000000000',
});
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', 'ready', '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<GerritChange>({
_number: 123456,
current_revision: 'some-revision',
created: t0.minus({ seconds: 30 }).toISO().replace('T', ' '),
revisions: {
'some-revision': partial<GerritRevisionInfo>({
commit_with_footers: 'Renovate-Branch: source',
}),
},
messages: [],
created: '2025-04-14 16:33:37.000000000',
});
clientMock.findChanges.mockResolvedValueOnce([change]);
const pr = await gerrit.createPr({
sourceBranch: 'source',
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',
'ready',
'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()', () => {
Expand Down Expand Up @@ -411,7 +513,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(
Expand Down Expand Up @@ -789,11 +891,9 @@ 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).toHaveBeenCalledExactlyOnceWith(123456, {
remove: ['hashtag1'],
});
});
});

Expand Down
Loading
Loading