Compare commits

...

27 commits

Author SHA1 Message Date
Felipe Santos
b8576e5855
Merge a6d9d910e6 into c12c57b2a8 2025-01-02 18:50:42 -03:00
Sebastian Poxhofer
c12c57b2a8
fix(managers/pep621): correctly parse extras with whitespace (#33378) 2025-01-02 21:24:39 +00:00
Michael Kriese
73b842fe3a
feat(manager/github-actions): support registry aliases (#33377) 2025-01-02 18:06:11 +00:00
Rhys Arkins
60754ce088
feat(presets): add RUSTC_BOOTSTRAP to safe global env (#33347)
Some checks are pending
Build / setup (push) Waiting to run
Build / setup-build (push) Waiting to run
Build / prefetch (push) Blocked by required conditions
Build / lint-eslint (push) Blocked by required conditions
Build / lint-prettier (push) Blocked by required conditions
Build / lint-docs (push) Blocked by required conditions
Build / lint-other (push) Blocked by required conditions
Build / (push) Blocked by required conditions
Build / codecov (push) Blocked by required conditions
Build / coverage-threshold (push) Blocked by required conditions
Build / test-success (push) Blocked by required conditions
Build / build (push) Blocked by required conditions
Build / build-docs (push) Blocked by required conditions
Build / test-e2e (push) Blocked by required conditions
Build / release (push) Blocked by required conditions
Code scanning / CodeQL-Build (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
whitesource-scan / WS_SCAN (push) Waiting to run
2025-01-02 17:20:40 +00:00
Felipe Santos
a6d9d910e6 refactor(gerrit): remove deprecated source branch as hashtags support 2024-12-29 16:10:26 +00:00
Felipe Santos
add6ca5720 Add tests for approvePrForAutomerge 2024-12-29 16:04:44 +00:00
Felipe Santos
b93ee7a628 Address review comments 2024-12-29 15:52:37 +00:00
Felipe Santos
74137a8460 Merge branch 'main' of https://github.com/renovatebot/renovate into fix-auto-merge-if-plus-one 2024-12-29 12:41:33 -03:00
Felipe Santos
e0b6f8d5ed chore: remove spurious comment 2024-12-02 17:41:35 +00:00
Felipe Santos
53743b27bf chore: fix type import 2024-12-02 17:38:39 +00:00
Felipe Santos
b1ce02b44b Merge branch 'main' of https://github.com/renovatebot/renovate into fix-auto-merge-if-plus-one 2024-12-02 17:36:43 +00:00
Felipe Santos
bb317ce921 chore(gerrit): improve approve method to avoid some API calls 2024-12-02 17:30:54 +00:00
Felipe Santos
4dfc817793 Revert "chore(gerrit): still approve if change was approved another user"
This reverts commit 5eb284603f.
2024-12-02 17:20:47 +00:00
Felipe Santos
c4c81e30fc chore: rename approvePr to approvePrForAutomerge 2024-12-02 17:19:39 +00:00
Felipe Santos
7923d5b955 Revert "feat(gitlab,azure): try approving before auto-merge"
This reverts commit c679c28d01.
2024-12-02 17:19:32 +00:00
Felipe Santos
539c12f744
Merge branch 'main' into fix-auto-merge-if-plus-one 2024-12-02 11:18:19 -03:00
Felipe Santos
ac30f2b204 test(automerge): add test for auto approve before merging 2024-12-01 17:32:33 +00:00
Felipe Santos
bd05816dd1 test(gerrit): fix lint in client.spec.ts 2024-12-01 17:28:48 +00:00
Felipe Santos
dbdf41a936 chore: add better jsdoc for approvePr 2024-12-01 17:27:27 +00:00
Felipe Santos
c679c28d01 feat(gitlab,azure): try approving before auto-merge 2024-12-01 17:17:00 +00:00
Felipe Santos
5eb284603f chore(gerrit): still approve if change was approved another user 2024-12-01 17:03:07 +00:00
Felipe Santos
1d8daf5095 test(gerrit): fix approvePr coverage 2024-12-01 16:40:25 +00:00
Felipe Santos
7877331cf7 Merge branch 'main' of https://github.com/renovatebot/renovate into fix-auto-merge-if-plus-one 2024-12-01 14:59:21 +00:00
Felipe Santos
6ffa5b615f Merge branch 'main' into fix-auto-merge-if-plus-one 2024-11-30 07:18:40 +00:00
Felipe Santos
6ac7b9e089 chore(gerrit): try auto-approve right before auto-merge 2024-11-30 07:12:25 +00:00
Felipe Santos
107a298054 chore(gerrit): improve auto-approve on new patchset workaround 2024-11-30 01:52:23 +00:00
Felipe Santos
46f8eed01f fix(gerrit): not auto-approving if change already had a Code-Review +1 2024-11-29 23:58:24 +00:00
20 changed files with 163 additions and 230 deletions

View file

@ -3707,6 +3707,7 @@ This feature works with the following managers:
- [`dockerfile`](modules/manager/dockerfile/index.md) - [`dockerfile`](modules/manager/dockerfile/index.md)
- [`droneci`](modules/manager/droneci/index.md) - [`droneci`](modules/manager/droneci/index.md)
- [`flux`](modules/manager/flux/index.md) - [`flux`](modules/manager/flux/index.md)
- [`github-actions`](modules/manager/github-actions/index.md)
- [`gitlabci`](modules/manager/gitlabci/index.md) - [`gitlabci`](modules/manager/gitlabci/index.md)
- [`helm-requirements`](modules/manager/helm-requirements/index.md) - [`helm-requirements`](modules/manager/helm-requirements/index.md)
- [`helm-values`](modules/manager/helm-values/index.md) - [`helm-values`](modules/manager/helm-values/index.md)

View file

@ -4,7 +4,7 @@ import type { Preset } from '../types';
export const presets: Record<string, Preset> = { export const presets: Record<string, Preset> = {
safeEnv: { safeEnv: {
allowedEnv: ['GO*'], allowedEnv: ['GO*', 'RUSTC_BOOTSTRAP'],
description: description:
'Hopefully safe environment variables to allow users to configure.', 'Hopefully safe environment variables to allow users to configure.',
}, },

View file

@ -10,7 +10,11 @@ import { GithubRunnersDatasource } from '../../datasource/github-runners';
import { GithubTagsDatasource } from '../../datasource/github-tags'; import { GithubTagsDatasource } from '../../datasource/github-tags';
import * as dockerVersioning from '../../versioning/docker'; import * as dockerVersioning from '../../versioning/docker';
import { getDep } from '../dockerfile/extract'; import { getDep } from '../dockerfile/extract';
import type { PackageDependency, PackageFileContent } from '../types'; import type {
ExtractConfig,
PackageDependency,
PackageFileContent,
} from '../types';
import type { Workflow } from './types'; import type { Workflow } from './types';
const dockerActionRe = regEx(/^\s+uses\s*: ['"]?docker:\/\/([^'"]+)\s*$/); const dockerActionRe = regEx(/^\s+uses\s*: ['"]?docker:\/\/([^'"]+)\s*$/);
@ -44,7 +48,10 @@ function detectCustomGitHubRegistryUrlsForActions(): PackageDependency {
return {}; return {};
} }
function extractWithRegex(content: string): PackageDependency[] { function extractWithRegex(
content: string,
config: ExtractConfig,
): PackageDependency[] {
const customRegistryUrlsPackageDependency = const customRegistryUrlsPackageDependency =
detectCustomGitHubRegistryUrlsForActions(); detectCustomGitHubRegistryUrlsForActions();
logger.trace('github-actions.extractWithRegex()'); logger.trace('github-actions.extractWithRegex()');
@ -57,7 +64,7 @@ function extractWithRegex(content: string): PackageDependency[] {
const dockerMatch = dockerActionRe.exec(line); const dockerMatch = dockerActionRe.exec(line);
if (dockerMatch) { if (dockerMatch) {
const [, currentFrom] = dockerMatch; const [, currentFrom] = dockerMatch;
const dep = getDep(currentFrom); const dep = getDep(currentFrom, true, config.registryAliases);
dep.depType = 'docker'; dep.depType = 'docker';
deps.push(dep); deps.push(dep);
continue; continue;
@ -126,11 +133,14 @@ function detectDatasource(registryUrl: string): PackageDependency {
}; };
} }
function extractContainer(container: unknown): PackageDependency | undefined { function extractContainer(
container: unknown,
registryAliases: Record<string, string> | undefined,
): PackageDependency | undefined {
if (is.string(container)) { if (is.string(container)) {
return getDep(container); return getDep(container, true, registryAliases);
} else if (is.plainObject(container) && is.string(container.image)) { } else if (is.plainObject(container) && is.string(container.image)) {
return getDep(container.image); return getDep(container.image, true, registryAliases);
} }
return undefined; return undefined;
} }
@ -181,6 +191,7 @@ function extractRunners(runner: unknown): PackageDependency[] {
function extractWithYAMLParser( function extractWithYAMLParser(
content: string, content: string,
packageFile: string, packageFile: string,
config: ExtractConfig,
): PackageDependency[] { ): PackageDependency[] {
logger.trace('github-actions.extractWithYAMLParser()'); logger.trace('github-actions.extractWithYAMLParser()');
const deps: PackageDependency[] = []; const deps: PackageDependency[] = [];
@ -198,14 +209,14 @@ function extractWithYAMLParser(
} }
for (const job of Object.values(pkg?.jobs ?? {})) { for (const job of Object.values(pkg?.jobs ?? {})) {
const dep = extractContainer(job?.container); const dep = extractContainer(job?.container, config.registryAliases);
if (dep) { if (dep) {
dep.depType = 'container'; dep.depType = 'container';
deps.push(dep); deps.push(dep);
} }
for (const service of Object.values(job?.services ?? {})) { for (const service of Object.values(job?.services ?? {})) {
const dep = extractContainer(service); const dep = extractContainer(service, config.registryAliases);
if (dep) { if (dep) {
dep.depType = 'service'; dep.depType = 'service';
deps.push(dep); deps.push(dep);
@ -221,11 +232,12 @@ function extractWithYAMLParser(
export function extractPackageFile( export function extractPackageFile(
content: string, content: string,
packageFile: string, packageFile: string,
config: ExtractConfig = {}, // TODO: enforce ExtractConfig
): PackageFileContent | null { ): PackageFileContent | null {
logger.trace(`github-actions.extractPackageFile(${packageFile})`); logger.trace(`github-actions.extractPackageFile(${packageFile})`);
const deps = [ const deps = [
...extractWithRegex(content), ...extractWithRegex(content, config),
...extractWithYAMLParser(content, packageFile), ...extractWithYAMLParser(content, packageFile, config),
]; ];
if (!deps.length) { if (!deps.length) {
return null; return null;

View file

@ -4,17 +4,18 @@ import { parsePEP508 } from './utils';
describe('modules/manager/pep621/utils', () => { describe('modules/manager/pep621/utils', () => {
describe('parsePEP508()', () => { describe('parsePEP508()', () => {
it.each` it.each`
value | success | packageName | currentValue | extras | marker value | success | packageName | currentValue | extras | marker
${''} | ${false} | ${undefined} | ${undefined} | ${undefined} | ${undefined} ${''} | ${false} | ${undefined} | ${undefined} | ${undefined} | ${undefined}
${undefined} | ${false} | ${undefined} | ${undefined} | ${undefined} | ${undefined} ${undefined} | ${false} | ${undefined} | ${undefined} | ${undefined} | ${undefined}
${null} | ${false} | ${undefined} | ${undefined} | ${undefined} | ${undefined} ${null} | ${false} | ${undefined} | ${undefined} | ${undefined} | ${undefined}
${'blinker'} | ${true} | ${'blinker'} | ${undefined} | ${undefined} | ${undefined} ${'blinker'} | ${true} | ${'blinker'} | ${undefined} | ${undefined} | ${undefined}
${'packaging==20.0.0'} | ${true} | ${'packaging'} | ${'==20.0.0'} | ${undefined} | ${undefined} ${'packaging==20.0.0'} | ${true} | ${'packaging'} | ${'==20.0.0'} | ${undefined} | ${undefined}
${'packaging>=20.9,!=22.0'} | ${true} | ${'packaging'} | ${'>=20.9,!=22.0'} | ${undefined} | ${undefined} ${'packaging>=20.9,!=22.0'} | ${true} | ${'packaging'} | ${'>=20.9,!=22.0'} | ${undefined} | ${undefined}
${'cachecontrol[filecache]>=0.12.11'} | ${true} | ${'cachecontrol'} | ${'>=0.12.11'} | ${['filecache']} | ${undefined} ${'cachecontrol[filecache]>=0.12.11'} | ${true} | ${'cachecontrol'} | ${'>=0.12.11'} | ${['filecache']} | ${undefined}
${'tomli>=1.1.0; python_version < "3.11"'} | ${true} | ${'tomli'} | ${'>=1.1.0'} | ${undefined} | ${'python_version < "3.11"'} ${'private-depB[extra1, extra2]~=2.4'} | ${true} | ${'private-depB'} | ${'~=2.4'} | ${['extra1', 'extra2']} | ${undefined}
${'typing-extensions; python_version < "3.8"'} | ${true} | ${'typing-extensions'} | ${undefined} | ${undefined} | ${'python_version < "3.8"'} ${'tomli>=1.1.0; python_version < "3.11"'} | ${true} | ${'tomli'} | ${'>=1.1.0'} | ${undefined} | ${'python_version < "3.11"'}
${'typing-extensions[test-feature]; python_version < "3.8"'} | ${true} | ${'typing-extensions'} | ${undefined} | ${['test-feature']} | ${'python_version < "3.8"'} ${'typing-extensions; python_version < "3.8"'} | ${true} | ${'typing-extensions'} | ${undefined} | ${undefined} | ${'python_version < "3.8"'}
${'typing-extensions[test-feature]; python_version < "3.8"'} | ${true} | ${'typing-extensions'} | ${undefined} | ${['test-feature']} | ${'python_version < "3.8"'}
`( `(
'(parse $value"', '(parse $value"',
({ value, success, packageName, currentValue, extras, marker }) => { ({ value, success, packageName, currentValue, extras, marker }) => {

View file

@ -10,7 +10,7 @@ import { PyProjectSchema } from './schema';
import type { Pep508ParseResult, Pep621ManagerData } from './types'; import type { Pep508ParseResult, Pep621ManagerData } from './types';
const pep508Regex = regEx( const pep508Regex = regEx(
/^(?<packageName>[A-Z0-9._-]+)\s*(\[(?<extras>[A-Z0-9,._-]+)\])?\s*(?<currentValue>[^;]+)?(;\s*(?<marker>.*))?/i, /^(?<packageName>[A-Z0-9._-]+)\s*(\[(?<extras>[A-Z0-9\s,._-]+)\])?\s*(?<currentValue>[^;]+)?(;\s*(?<marker>.*))?/i,
); );
export const depTypes = { export const depTypes = {
@ -49,7 +49,8 @@ export function parsePEP508(
result.marker = regExpExec.groups.marker; result.marker = regExpExec.groups.marker;
} }
if (is.nonEmptyString(regExpExec.groups.extras)) { if (is.nonEmptyString(regExpExec.groups.extras)) {
result.extras = regExpExec.groups.extras.split(','); // trim to remove allowed whitespace between brackets
result.extras = regExpExec.groups.extras.split(',').map((e) => e.trim());
} }
return result; return result;

View file

@ -102,7 +102,6 @@ describe('modules/platform/gerrit/client', () => {
'footer:Renovate-Branch=dependency-xyz', 'footer:Renovate-Branch=dependency-xyz',
{ branchName: 'dependency-xyz' }, { branchName: 'dependency-xyz' },
], ],
['hashtag:sourceBranch-dependency-xyz', { branchName: 'dependency-xyz' }], // for backwards compatibility
['label:Code-Review=-2', { branchName: 'dependency-xyz', label: '-2' }], ['label:Code-Review=-2', { branchName: 'dependency-xyz', label: '-2' }],
[ [
'branch:otherTarget', 'branch:otherTarget',
@ -399,10 +398,20 @@ describe('modules/platform/gerrit/client', () => {
describe('approveChange()', () => { describe('approveChange()', () => {
it('already approved - do nothing', async () => { it('already approved - do nothing', async () => {
const change = partial<GerritChange>({}); const change = partial<GerritChange>({
labels: {
'Code-Review': {
all: [{ value: 2 }],
},
},
});
httpMock httpMock
.scope(gerritEndpointUrl) .scope(gerritEndpointUrl)
.get((url) => url.includes('/a/changes/123456?o=')) .get(
(url) =>
url.includes('/a/changes/123456?o=') &&
url.includes('o=DETAILED_LABELS'),
)
.reply(200, gerritRestResponse(change), jsonResultHeader); .reply(200, gerritRestResponse(change), jsonResultHeader);
await expect(client.approveChange(123456)).toResolve(); await expect(client.approveChange(123456)).toResolve();
}); });
@ -411,17 +420,54 @@ describe('modules/platform/gerrit/client', () => {
const change = partial<GerritChange>({ labels: {} }); const change = partial<GerritChange>({ labels: {} });
httpMock httpMock
.scope(gerritEndpointUrl) .scope(gerritEndpointUrl)
.get((url) => url.includes('/a/changes/123456?o=')) .get(
(url) =>
url.includes('/a/changes/123456?o=') &&
url.includes('o=DETAILED_LABELS'),
)
.reply(200, gerritRestResponse(change), jsonResultHeader); .reply(200, gerritRestResponse(change), jsonResultHeader);
await expect(client.approveChange(123456)).toResolve(); await expect(client.approveChange(123456)).toResolve();
}); });
it('not already approved - approve now', async () => { it('not already approved - approve now', async () => {
const change = partial<GerritChange>({ labels: { 'Code-Review': {} } }); const change = partial<GerritChange>({
labels: { 'Code-Review': { all: [] } },
});
httpMock httpMock
.scope(gerritEndpointUrl) .scope(gerritEndpointUrl)
.get((url) => url.includes('/a/changes/123456?o=')) .get(
(url) =>
url.includes('/a/changes/123456?o=') &&
url.includes('o=DETAILED_LABELS'),
)
.reply(200, gerritRestResponse(change), jsonResultHeader);
const approveMock = httpMock
.scope(gerritEndpointUrl)
.post('/a/changes/123456/revisions/current/review', {
labels: { 'Code-Review': +2 },
notify: 'NONE',
})
.reply(200, gerritRestResponse(''), jsonResultHeader);
await expect(client.approveChange(123456)).toResolve();
expect(approveMock.isDone()).toBeTrue();
});
it('not already approved because of +1 - approve now', async () => {
const change = partial<GerritChange>({
labels: {
'Code-Review': {
all: [{ value: 1 }],
},
},
});
httpMock
.scope(gerritEndpointUrl)
.get(
(url) =>
url.includes('/a/changes/123456?o=') &&
url.includes('o=DETAILED_LABELS'),
)
.reply(200, gerritRestResponse(change), jsonResultHeader); .reply(200, gerritRestResponse(change), jsonResultHeader);
const approveMock = httpMock const approveMock = httpMock
.scope(gerritEndpointUrl) .scope(gerritEndpointUrl)
@ -434,63 +480,6 @@ describe('modules/platform/gerrit/client', () => {
expect(approveMock.isDone()).toBeTrue(); expect(approveMock.isDone()).toBeTrue();
}); });
}); });
describe('wasApprovedBy()', () => {
it('label not exists', () => {
expect(
client.wasApprovedBy(partial<GerritChange>({}), 'user'),
).toBeUndefined();
});
it('not approved by anyone', () => {
expect(
client.wasApprovedBy(
partial<GerritChange>({
labels: {
'Code-Review': {},
},
}),
'user',
),
).toBeUndefined();
});
it('approved by given user', () => {
expect(
client.wasApprovedBy(
partial<GerritChange>({
labels: {
'Code-Review': {
approved: {
_account_id: 1,
username: 'user',
},
},
},
}),
'user',
),
).toBeTrue();
});
it('approved by given other', () => {
expect(
client.wasApprovedBy(
partial<GerritChange>({
labels: {
'Code-Review': {
approved: {
_account_id: 1,
username: 'other',
},
},
},
}),
'user',
),
).toBeFalse();
});
});
}); });
function gerritRestResponse(body: any): any { function gerritRestResponse(body: any): any {

View file

@ -2,6 +2,7 @@ import { REPOSITORY_ARCHIVED } from '../../../constants/error-messages';
import { logger } from '../../../logger'; import { logger } from '../../../logger';
import { GerritHttp } from '../../../util/http/gerrit'; import { GerritHttp } from '../../../util/http/gerrit';
import { regEx } from '../../../util/regex'; import { regEx } from '../../../util/regex';
import { getQueryString } from '../../../util/url';
import type { import type {
GerritAccountInfo, GerritAccountInfo,
GerritBranchInfo, GerritBranchInfo,
@ -72,10 +73,14 @@ class GerritClient {
return changes.body; return changes.body;
} }
async getChange(changeNumber: number): Promise<GerritChange> { async getChange(
changeNumber: number,
extraOptions?: string[],
): Promise<GerritChange> {
const options = [...this.requestDetails, ...(extraOptions ?? [])];
const queryString = getQueryString({ o: options });
const changes = await this.gerritHttp.getJson<GerritChange>( const changes = await this.gerritHttp.getJson<GerritChange>(
`a/changes/${changeNumber}?` + `a/changes/${changeNumber}?${queryString}`,
this.requestDetails.map((det) => `o=${det}`).join('&'),
); );
return changes.body; return changes.body;
} }
@ -196,15 +201,11 @@ class GerritClient {
} }
async checkIfApproved(changeId: number): Promise<boolean> { async checkIfApproved(changeId: number): Promise<boolean> {
const change = await client.getChange(changeId); const change = await client.getChange(changeId, ['DETAILED_LABELS']);
const reviewLabels = change?.labels?.['Code-Review']; const reviewLabel = change?.labels?.['Code-Review'];
return reviewLabels === undefined || reviewLabels.approved !== undefined;
}
wasApprovedBy(change: GerritChange, username: string): boolean | undefined {
return ( return (
change.labels?.['Code-Review'].approved && reviewLabel === undefined ||
change.labels['Code-Review'].approved.username === username reviewLabel.all?.some((label) => label.value === 2) === true
); );
} }
@ -220,16 +221,7 @@ class GerritClient {
const filterState = mapPrStateToGerritFilter(searchConfig.state); const filterState = mapPrStateToGerritFilter(searchConfig.state);
const filters = ['owner:self', 'project:' + repository, filterState]; const filters = ['owner:self', 'project:' + repository, filterState];
if (searchConfig.branchName) { if (searchConfig.branchName) {
filters.push( filters.push(`footer:Renovate-Branch=${searchConfig.branchName}`);
...[
'(',
`footer:Renovate-Branch=${searchConfig.branchName}`,
// for backwards compatibility
'OR',
`hashtag:sourceBranch-${searchConfig.branchName}`,
')',
],
);
} }
if (searchConfig.targetBranch) { if (searchConfig.targetBranch) {
filters.push(`branch:${searchConfig.targetBranch}`); filters.push(`branch:${searchConfig.targetBranch}`);

View file

@ -187,28 +187,6 @@ describe('modules/platform/gerrit/index', () => {
gerrit.writeToConfig({ labels: {} }); gerrit.writeToConfig({ labels: {} });
}); });
it('updatePr() - auto approve enabled', async () => {
const change = partial<GerritChange>({
current_revision: 'some-revision',
revisions: {
'some-revision': partial<GerritRevisionInfo>({
commit: {
message: 'some message',
},
}),
},
});
clientMock.getChange.mockResolvedValueOnce(change);
await gerrit.updatePr({
number: 123456,
prTitle: 'subject',
platformPrOptions: {
autoApprove: true,
},
});
expect(clientMock.approveChange).toHaveBeenCalledWith(123456);
});
it('updatePr() - closed => abandon the change', async () => { it('updatePr() - closed => abandon the change', async () => {
const change = partial<GerritChange>({}); const change = partial<GerritChange>({});
clientMock.getChange.mockResolvedValueOnce(change); clientMock.getChange.mockResolvedValueOnce(change);
@ -309,7 +287,7 @@ describe('modules/platform/gerrit/index', () => {
]); ]);
}); });
it('createPr() - update body WITHOUT approve', async () => { it('createPr() - update body', async () => {
const pr = await gerrit.createPr({ const pr = await gerrit.createPr({
sourceBranch: 'source', sourceBranch: 'source',
targetBranch: 'target', targetBranch: 'target',
@ -325,26 +303,6 @@ describe('modules/platform/gerrit/index', () => {
'body', 'body',
TAG_PULL_REQUEST_BODY, TAG_PULL_REQUEST_BODY,
); );
expect(clientMock.approveChange).not.toHaveBeenCalled();
});
it('createPr() - update body and approve', async () => {
const pr = await gerrit.createPr({
sourceBranch: 'source',
targetBranch: 'target',
prTitle: change.subject,
prBody: 'body',
platformPrOptions: {
autoApprove: true,
},
});
expect(pr).toHaveProperty('number', 123456);
expect(clientMock.addMessageIfNotAlreadyExists).toHaveBeenCalledWith(
123456,
'body',
TAG_PULL_REQUEST_BODY,
);
expect(clientMock.approveChange).toHaveBeenCalledWith(123456);
}); });
}); });
@ -750,6 +708,13 @@ describe('modules/platform/gerrit/index', () => {
//TODO: add some tests for Gerrit-specific replacements.. //TODO: add some tests for Gerrit-specific replacements..
}); });
describe('approvePrForAutomerge()', () => {
it('approvePrForAutomerge() - calls approveChange', async () => {
await expect(gerrit.approvePrForAutomerge(123456)).toResolve();
expect(clientMock.approveChange).toHaveBeenCalledWith(123456);
});
});
describe('currently unused/not-implemented functions', () => { describe('currently unused/not-implemented functions', () => {
it('deleteLabel()', async () => { it('deleteLabel()', async () => {
await expect( await expect(

View file

@ -160,9 +160,6 @@ export async function updatePr(prConfig: UpdatePrConfig): Promise<void> {
TAG_PULL_REQUEST_BODY, TAG_PULL_REQUEST_BODY,
); );
} }
if (prConfig.platformPrOptions?.autoApprove) {
await client.approveChange(prConfig.number);
}
if (prConfig.state && prConfig.state === 'closed') { if (prConfig.state && prConfig.state === 'closed') {
await client.abandonChange(prConfig.number); await client.abandonChange(prConfig.number);
} }
@ -195,9 +192,6 @@ export async function createPr(prConfig: CreatePRConfig): Promise<Pr | null> {
prConfig.prBody, prConfig.prBody,
TAG_PULL_REQUEST_BODY, TAG_PULL_REQUEST_BODY,
); );
if (prConfig.platformPrOptions?.autoApprove) {
await client.approveChange(pr._number);
}
return getPr(pr._number); return getPr(pr._number);
} }
@ -442,3 +436,14 @@ export function findIssue(title: string): Promise<Issue | null> {
export function getIssueList(): Promise<Issue[]> { export function getIssueList(): Promise<Issue[]> {
return Promise.resolve([]); return Promise.resolve([]);
} }
/**
* The Code-Review +2 vote of when the change was created or updated in Gerrit
* may have been downgraded by a CI check utilizing the same account as
* Renovate (e.g. SonarQube which posts Code-Review +1). This function will
* vote with +2 again on the change, if needed, before Renovate attempt to
* automerge it.
*/
export async function approvePrForAutomerge(number: number): Promise<void> {
await client.approveChange(number);
}

View file

@ -8,12 +8,6 @@ Support for Gerrit is currently _experimental_, meaning that it _might_ still ha
Renovate stores its metadata in the _commit message footer_. Renovate stores its metadata in the _commit message footer_.
Previously Renovate stored metadata in Gerrit's _hashtags_.
To keep backwards compatibility, Renovate still reads metadata from hashtags.
But Renovate _always_ puts its metadata in the _commit message footer_!
When the Renovate maintainers mark Gerrit support as stable, the maintainers will remove the "read metadata from hashtags" feature.
This means changes without metadata in the commit message footer will be "forgotten" by Renovate.
## Authentication ## Authentication
<figure markdown> <figure markdown>

View file

@ -369,7 +369,6 @@ describe('modules/platform/gerrit/scm', () => {
}, },
}); });
clientMock.findChanges.mockResolvedValueOnce([existingChange]); clientMock.findChanges.mockResolvedValueOnce([existingChange]);
clientMock.wasApprovedBy.mockReturnValueOnce(true);
git.prepareCommit.mockResolvedValueOnce({ git.prepareCommit.mockResolvedValueOnce({
commitSha: 'commitSha' as LongCommitSha, commitSha: 'commitSha' as LongCommitSha,
parentCommitSha: 'parentSha' as LongCommitSha, parentCommitSha: 'parentSha' as LongCommitSha,
@ -385,6 +384,7 @@ describe('modules/platform/gerrit/scm', () => {
message: 'commit msg', message: 'commit msg',
files: [], files: [],
prTitle: 'pr title', prTitle: 'pr title',
autoApprove: true,
}), }),
).toBe('commitSha'); ).toBe('commitSha');
expect(git.prepareCommit).toHaveBeenCalledWith({ expect(git.prepareCommit).toHaveBeenCalledWith({
@ -396,19 +396,15 @@ describe('modules/platform/gerrit/scm', () => {
'Renovate-Branch: renovate/dependency-1.x\nChange-Id: ...', 'Renovate-Branch: renovate/dependency-1.x\nChange-Id: ...',
], ],
prTitle: 'pr title', prTitle: 'pr title',
autoApprove: true,
force: true, force: true,
}); });
expect(git.fetchRevSpec).toHaveBeenCalledWith('refs/changes/1/2'); expect(git.fetchRevSpec).toHaveBeenCalledWith('refs/changes/1/2');
expect(git.pushCommit).toHaveBeenCalledWith({ expect(git.pushCommit).toHaveBeenCalledWith({
files: [], files: [],
sourceRef: 'renovate/dependency-1.x', sourceRef: 'renovate/dependency-1.x',
targetRef: 'refs/for/main%notify=NONE', targetRef: 'refs/for/main%notify=NONE,l=Code-Review+2',
}); });
expect(clientMock.wasApprovedBy).toHaveBeenCalledWith(
existingChange,
'user',
);
expect(clientMock.approveChange).toHaveBeenCalledWith(123456);
}); });
}); });
}); });

View file

@ -129,19 +129,16 @@ export class GerritScm extends DefaultGitScm {
hasChanges = await git.hasDiff('HEAD', 'FETCH_HEAD'); //avoid empty patchsets hasChanges = await git.hasDiff('HEAD', 'FETCH_HEAD'); //avoid empty patchsets
} }
if (hasChanges || commit.force) { if (hasChanges || commit.force) {
const pushOptions = ['notify=NONE'];
if (commit.autoApprove) {
pushOptions.push('l=Code-Review+2');
}
const pushResult = await git.pushCommit({ const pushResult = await git.pushCommit({
sourceRef: commit.branchName, sourceRef: commit.branchName,
targetRef: `refs/for/${commit.baseBranch!}%notify=NONE`, targetRef: `refs/for/${commit.baseBranch!}%${pushOptions.join(',')}`,
files: commit.files, files: commit.files,
}); });
if (pushResult) { if (pushResult) {
//existingChange was the old change before commit/push. we need to approve again, if it was previously approved from renovate
if (
existingChange &&
client.wasApprovedBy(existingChange, username)
) {
await client.approveChange(existingChange._number);
}
return commitSha; return commitSha;
} }
} }

View file

@ -34,10 +34,6 @@ export type GerritReviewersType = 'REVIEWER' | 'CC' | 'REMOVED';
export interface GerritChange { export interface GerritChange {
branch: string; branch: string;
/**
* for backwards compatibility
*/
hashtags?: string[];
change_id: string; change_id: string;
subject: string; subject: string;
status: GerritChangeStatus; status: GerritChangeStatus;
@ -79,6 +75,8 @@ export interface GerritLabelInfo {
rejected?: GerritAccountInfo; rejected?: GerritAccountInfo;
/** If true, the label blocks submit operation. If not set, the default is false. */ /** If true, the label blocks submit operation. If not set, the default is false. */
blocking?: boolean; blocking?: boolean;
/** List of votes. Only set when o=DETAILED_LABELS. */
all?: GerritApprovalInfo[];
} }
export interface GerritActionInfo { export interface GerritActionInfo {
@ -101,3 +99,8 @@ export interface GerritMergeableInfo {
| 'CHERRY_PICK'; | 'CHERRY_PICK';
mergeable: boolean; mergeable: boolean;
} }
export interface GerritApprovalInfo {
value?: number;
username?: string;
}

View file

@ -186,47 +186,6 @@ describe('modules/platform/gerrit/utils', () => {
}); });
expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x'); expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x');
}); });
// for backwards compatibility
it('no commit message but with hashtags', () => {
const change = partial<GerritChange>({
hashtags: ['sourceBranch-renovate/dependency-1.x'],
});
expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x');
});
// for backwards compatibility
it('commit message with no footer but with hashtags', () => {
const change = partial<GerritChange>({
hashtags: ['sourceBranch-renovate/dependency-1.x'],
current_revision: 'abc',
revisions: {
abc: partial<GerritRevisionInfo>({
commit: {
message: 'some message...',
},
}),
},
});
expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-1.x');
});
// for backwards compatibility
it('prefers the footer over the hashtags', () => {
const change = partial<GerritChange>({
hashtags: ['sourceBranch-renovate/dependency-1.x'],
current_revision: 'abc',
revisions: {
abc: partial<GerritRevisionInfo>({
commit: {
message:
'Some change\n\nRenovate-Branch: renovate/dependency-2.x\nChange-Id: ...',
},
}),
},
});
expect(utils.extractSourceBranch(change)).toBe('renovate/dependency-2.x');
});
}); });
describe('findPullRequestBody()', () => { describe('findPullRequestBody()', () => {

View file

@ -101,13 +101,6 @@ export function extractSourceBranch(change: GerritChange): string | undefined {
} }
} }
// for backwards compatibility
if (!sourceBranch) {
sourceBranch = change.hashtags
?.find((tag) => tag.startsWith('sourceBranch-'))
?.replace('sourceBranch-', '');
}
return sourceBranch ?? undefined; return sourceBranch ?? undefined;
} }

View file

@ -283,6 +283,11 @@ export interface Platform {
maxBodyLength(): number; maxBodyLength(): number;
labelCharLimit?(): number; labelCharLimit?(): number;
/**
* Platforms that support `autoApprove` can implement this function. It will be
* invoked during automerge before the PR branch status is checked.
*/
approvePrForAutomerge?(number: number): Promise<void>;
} }
export interface PlatformScm { export interface PlatformScm {

View file

@ -86,6 +86,8 @@ export interface CommitFilesConfig {
platformCommit?: PlatformCommitOptions; platformCommit?: PlatformCommitOptions;
/** Only needed by Gerrit platform */ /** Only needed by Gerrit platform */
prTitle?: string; prTitle?: string;
/** Only needed by Gerrit platform */
autoApprove?: boolean;
} }
export interface PushFilesConfig { export interface PushFilesConfig {

View file

@ -62,5 +62,7 @@ export function commitFilesToBranch(
platformCommit: config.platformCommit, platformCommit: config.platformCommit,
// Only needed by Gerrit platform // Only needed by Gerrit platform
prTitle: config.prTitle, prTitle: config.prTitle,
// Only needed by Gerrit platform
autoApprove: config.autoApprove,
}); });
} }

View file

@ -124,6 +124,18 @@ describe('workers/repository/update/pr/automerge', () => {
expect(platform.mergePr).toHaveBeenCalledTimes(0); expect(platform.mergePr).toHaveBeenCalledTimes(0);
}); });
it('should attempt to auto-approve if supported', async () => {
config.autoApprove = true;
config.automerge = true;
config.pruneBranchAfterAutomerge = true;
platform.getBranchStatus.mockResolvedValueOnce('green');
platform.mergePr.mockResolvedValueOnce(true);
const res = await prAutomerge.checkAutoMerge(pr, config);
expect(res).toEqual({ automerged: true, branchRemoved: true });
expect(platform.approvePrForAutomerge).toHaveBeenCalledTimes(1);
expect(platform.mergePr).toHaveBeenCalledTimes(1);
});
it('should not automerge if enabled and pr is unmergeable', async () => { it('should not automerge if enabled and pr is unmergeable', async () => {
config.automerge = true; config.automerge = true;
scm.isBranchConflicted.mockResolvedValueOnce(true); scm.isBranchConflicted.mockResolvedValueOnce(true);

View file

@ -69,6 +69,10 @@ export async function checkAutoMerge(
prAutomergeBlockReason: 'PlatformNotReady', prAutomergeBlockReason: 'PlatformNotReady',
}; };
} }
if (config.autoApprove && platform.approvePrForAutomerge) {
logger.debug('Auto-approving PR for automerge');
await platform.approvePrForAutomerge(pr.number);
}
const branchStatus = await resolveBranchStatus( const branchStatus = await resolveBranchStatus(
branchName, branchName,
!!config.internalChecksAsSuccess, !!config.internalChecksAsSuccess,