mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-10 14:06:30 +00:00
Compare commits
27 commits
4fc34de7a0
...
b8576e5855
Author | SHA1 | Date | |
---|---|---|---|
|
b8576e5855 | ||
|
c12c57b2a8 | ||
|
73b842fe3a | ||
|
60754ce088 | ||
|
a6d9d910e6 | ||
|
add6ca5720 | ||
|
b93ee7a628 | ||
|
74137a8460 | ||
|
e0b6f8d5ed | ||
|
53743b27bf | ||
|
b1ce02b44b | ||
|
bb317ce921 | ||
|
4dfc817793 | ||
|
c4c81e30fc | ||
|
7923d5b955 | ||
|
539c12f744 | ||
|
ac30f2b204 | ||
|
bd05816dd1 | ||
|
dbdf41a936 | ||
|
c679c28d01 | ||
|
5eb284603f | ||
|
1d8daf5095 | ||
|
7877331cf7 | ||
|
6ffa5b615f | ||
|
6ac7b9e089 | ||
|
107a298054 | ||
|
46f8eed01f |
20 changed files with 163 additions and 230 deletions
|
@ -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)
|
||||||
|
|
|
@ -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.',
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}`);
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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()', () => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue