mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-10 22:16:28 +00:00
Merge ce17e9c753
into 809911a843
This commit is contained in:
commit
f6035da243
10 changed files with 536 additions and 258 deletions
|
@ -271,6 +271,7 @@ export interface RenovateConfig
|
|||
packageFile?: string;
|
||||
packageRules?: PackageRule[];
|
||||
postUpdateOptions?: string[];
|
||||
branchConcurrentLimit?: number | null;
|
||||
prConcurrentLimit?: number;
|
||||
prHourlyLimit?: number;
|
||||
forkModeDisallowMaintainerEdits?: boolean;
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import { partial } from '../../../test/util';
|
||||
import type { BranchConfig, BranchUpgradeConfig } from '../types';
|
||||
import {
|
||||
calcLimit,
|
||||
hasMultipleLimits,
|
||||
incCountValue,
|
||||
incLimitedValue,
|
||||
isLimitReached,
|
||||
resetAllLimits,
|
||||
setCount,
|
||||
setMaxLimit,
|
||||
} from './limits';
|
||||
|
||||
|
@ -60,4 +66,242 @@ describe('workers/global/limits', () => {
|
|||
setMaxLimit('Commits', -1000);
|
||||
expect(isLimitReached('Commits')).toBeTrue();
|
||||
});
|
||||
|
||||
describe('calcLimit', () => {
|
||||
it('handles single upgrade', () => {
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(calcLimit(upgrades, 'prHourlyLimit')).toBe(10);
|
||||
expect(calcLimit(upgrades, 'branchConcurrentLimit')).toBe(11);
|
||||
expect(calcLimit(upgrades, 'prConcurrentLimit')).toBe(12);
|
||||
});
|
||||
|
||||
it('inherits prConcurrentLimit if branchConcurrentLimit is null', () => {
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: null,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(calcLimit(upgrades, 'prHourlyLimit')).toBe(10);
|
||||
expect(calcLimit(upgrades, 'branchConcurrentLimit')).toBe(12);
|
||||
expect(calcLimit(upgrades, 'prConcurrentLimit')).toBe(12);
|
||||
});
|
||||
|
||||
it('returns 0 if atleast one upgrade has no limit in the branch', () => {
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 0,
|
||||
branchConcurrentLimit: 0,
|
||||
prConcurrentLimit: 0,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 1,
|
||||
branchConcurrentLimit: 1,
|
||||
prConcurrentLimit: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(calcLimit(upgrades, 'prHourlyLimit')).toBe(0);
|
||||
expect(calcLimit(upgrades, 'branchConcurrentLimit')).toBe(0);
|
||||
expect(calcLimit(upgrades, 'prConcurrentLimit')).toBe(0);
|
||||
});
|
||||
|
||||
it('computes the lowest limit if multiple limits are present', () => {
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 1,
|
||||
branchConcurrentLimit: 1,
|
||||
prConcurrentLimit: 1,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 5,
|
||||
branchConcurrentLimit: 6,
|
||||
prConcurrentLimit: 3,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 5,
|
||||
branchConcurrentLimit: null,
|
||||
prConcurrentLimit: undefined,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 5,
|
||||
branchConcurrentLimit: 6,
|
||||
prConcurrentLimit: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(calcLimit(upgrades, 'prHourlyLimit')).toBe(1);
|
||||
expect(calcLimit(upgrades, 'branchConcurrentLimit')).toBe(1);
|
||||
expect(calcLimit(upgrades, 'prConcurrentLimit')).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasMultipleLimits', () => {
|
||||
it('handles single limit', () => {
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
]);
|
||||
expect(hasMultipleLimits(upgrades, 'prHourlyLimit')).toBe(false);
|
||||
expect(hasMultipleLimits(upgrades, 'branchConcurrentLimit')).toBe(false);
|
||||
expect(hasMultipleLimits(upgrades, 'prConcurrentLimit')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if there are multiple limits with value', () => {
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
]);
|
||||
expect(hasMultipleLimits(upgrades, 'prHourlyLimit')).toBe(false);
|
||||
expect(hasMultipleLimits(upgrades, 'branchConcurrentLimit')).toBe(false);
|
||||
expect(hasMultipleLimits(upgrades, 'prConcurrentLimit')).toBe(false);
|
||||
});
|
||||
|
||||
it('handles multiple limits', () => {
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 11,
|
||||
branchConcurrentLimit: 12,
|
||||
prConcurrentLimit: 13,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 0,
|
||||
branchConcurrentLimit: null,
|
||||
prConcurrentLimit: 3,
|
||||
},
|
||||
]);
|
||||
expect(hasMultipleLimits(upgrades, 'prHourlyLimit')).toBe(true);
|
||||
expect(hasMultipleLimits(upgrades, 'branchConcurrentLimit')).toBe(true);
|
||||
expect(hasMultipleLimits(upgrades, 'prConcurrentLimit')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLimitReached', () => {
|
||||
it('returns false based on concurrent limits', () => {
|
||||
setCount('ConcurrentPRs', 1);
|
||||
setCount('HourlyPRs', 1);
|
||||
incCountValue('Branches'); // using incCountValue so it gets test coverage
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 11,
|
||||
branchConcurrentLimit: 12,
|
||||
prConcurrentLimit: 13,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 0,
|
||||
branchConcurrentLimit: null,
|
||||
prConcurrentLimit: 3,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
isLimitReached('Branches', partial<BranchConfig>({ upgrades })),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isLimitReached('ConcurrentPRs', partial<BranchConfig>({ upgrades })),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when hourly limit is reached', () => {
|
||||
setCount('Branches', 2);
|
||||
setCount('ConcurrentPRs', 2);
|
||||
setCount('HourlyPRs', 2);
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 11,
|
||||
branchConcurrentLimit: 12,
|
||||
prConcurrentLimit: 13,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 2,
|
||||
branchConcurrentLimit: null,
|
||||
prConcurrentLimit: 3,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
isLimitReached('Branches', partial<BranchConfig>({ upgrades })),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isLimitReached('ConcurrentPRs', partial<BranchConfig>({ upgrades })),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when concurrent limit is reached', () => {
|
||||
setCount('Branches', 3);
|
||||
setCount('ConcurrentPRs', 3);
|
||||
setCount('HourlyPRs', 4);
|
||||
const upgrades = partial<BranchUpgradeConfig>([
|
||||
{
|
||||
prHourlyLimit: 10,
|
||||
branchConcurrentLimit: 11,
|
||||
prConcurrentLimit: 12,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 11,
|
||||
branchConcurrentLimit: 12,
|
||||
prConcurrentLimit: 13,
|
||||
},
|
||||
{
|
||||
prHourlyLimit: 5,
|
||||
branchConcurrentLimit: null,
|
||||
prConcurrentLimit: 3,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
isLimitReached('Branches', partial<BranchConfig>({ upgrades })),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isLimitReached('ConcurrentPRs', partial<BranchConfig>({ upgrades })),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import is from '@sindresorhus/is';
|
||||
import { logger } from '../../logger';
|
||||
import type { BranchConfig, BranchUpgradeConfig } from '../types';
|
||||
|
||||
export type Limit = 'Commits' | 'PullRequests' | 'Branches';
|
||||
|
||||
export type Limit = 'Commits';
|
||||
interface LimitValue {
|
||||
max: number | null;
|
||||
current: number;
|
||||
|
@ -27,8 +28,8 @@ export function incLimitedValue(key: Limit, incBy = 1): void {
|
|||
});
|
||||
}
|
||||
|
||||
export function isLimitReached(key: Limit): boolean {
|
||||
const limit = limits.get(key);
|
||||
function handleCommitsLimit(): boolean {
|
||||
const limit = limits.get('Commits');
|
||||
// TODO: fix me?
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
||||
if (!limit || limit.max === null) {
|
||||
|
@ -37,3 +38,162 @@ export function isLimitReached(key: Limit): boolean {
|
|||
const { max, current } = limit;
|
||||
return max - current <= 0;
|
||||
}
|
||||
|
||||
export type CountName = 'ConcurrentPRs' | 'HourlyPRs' | 'Branches';
|
||||
|
||||
type BranchLimitName =
|
||||
| 'branchConcurrentLimit'
|
||||
| 'prConcurrentLimit'
|
||||
| 'prHourlyLimit';
|
||||
|
||||
export const counts = new Map<CountName, number>();
|
||||
|
||||
export function getCount(key: CountName): number {
|
||||
const count = counts.get(key);
|
||||
// istanbul ignore if: should not happen
|
||||
if (!count) {
|
||||
logger.warn(`Could not compute the count of ${key}, returning zero.`);
|
||||
return 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function setCount(key: CountName, val: number): void {
|
||||
counts.set(key, val);
|
||||
logger.debug(`${key} count = ${val}`);
|
||||
}
|
||||
|
||||
export function incCountValue(key: CountName, incBy = 1): void {
|
||||
const count = getCount(key);
|
||||
counts.set(key, count + incBy);
|
||||
}
|
||||
|
||||
function handleConcurrentLimits(
|
||||
key: Exclude<CountName, 'HourlyPRs'>,
|
||||
config: BranchConfig,
|
||||
): boolean {
|
||||
const limitKey =
|
||||
key === 'Branches' ? 'branchConcurrentLimit' : 'prConcurrentLimit';
|
||||
|
||||
// calculate the limits for this branch
|
||||
const hourlyLimit = calcLimit(config.upgrades, 'prHourlyLimit');
|
||||
const hourlyPrCount = getCount('HourlyPRs');
|
||||
|
||||
// if a limit is defined ( >0 ) and limit reached return true ie. limit has been reached
|
||||
if (hourlyLimit && hourlyPrCount >= hourlyLimit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const limitValue = calcLimit(config.upgrades, limitKey);
|
||||
const currentCount = getCount(key);
|
||||
|
||||
if (limitValue && currentCount >= limitValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function calcLimit(
|
||||
upgrades: BranchUpgradeConfig[],
|
||||
limitName: BranchLimitName,
|
||||
): number {
|
||||
logger.debug(
|
||||
{
|
||||
limits: upgrades.map((upg) => {
|
||||
return { depName: upg.depName, [limitName]: upg[limitName] };
|
||||
}),
|
||||
},
|
||||
`${limitName} of the upgrades present in this branch`,
|
||||
);
|
||||
|
||||
if (hasMultipleLimits(upgrades, limitName)) {
|
||||
logger.once.debug(
|
||||
`Branch has multiple ${limitName} limits. The lowest among these will be selected.`,
|
||||
);
|
||||
}
|
||||
|
||||
let lowestLimit = Number.MAX_SAFE_INTEGER;
|
||||
for (const upgrade of upgrades) {
|
||||
let limit = upgrade[limitName];
|
||||
|
||||
// inherit prConcurrentLimit value incase branchConcurrentLimit is null
|
||||
if (!is.number(limit) && limitName === 'branchConcurrentLimit') {
|
||||
limit = upgrade.prConcurrentLimit;
|
||||
}
|
||||
|
||||
// istanbul ignore if: should never happen as all limits get a default value
|
||||
if (is.undefined(limit)) {
|
||||
limit = Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
// no limit
|
||||
if (limit === 0 || limit === null) {
|
||||
logger.debug(
|
||||
`${limitName} of this branch is unlimited, because atleast one of the upgrade has it's ${limitName} set to "No limit" ie. 0 or null`,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// limit is set
|
||||
lowestLimit = limit < lowestLimit ? limit : lowestLimit;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Calculated lowest ${limitName} among the upgrades present in this branch is ${lowestLimit}.`,
|
||||
);
|
||||
return lowestLimit;
|
||||
}
|
||||
|
||||
export function hasMultipleLimits(
|
||||
upgrades: BranchUpgradeConfig[],
|
||||
limitName: BranchLimitName,
|
||||
): boolean {
|
||||
if (upgrades.length === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const distinctLimits = new Set<number>();
|
||||
for (const upgrade of upgrades) {
|
||||
let limitValue = upgrade[limitName];
|
||||
|
||||
// inherit prConcurrentLimit value incase branchConcurrentLimit is null
|
||||
if (limitName === 'branchConcurrentLimit' && !is.number(limitValue)) {
|
||||
limitValue = upgrade.prConcurrentLimit;
|
||||
}
|
||||
|
||||
// istanbul ignore if: should not happen as the limits are of type number
|
||||
if (limitValue === null) {
|
||||
limitValue = 0;
|
||||
}
|
||||
|
||||
if (!is.undefined(limitValue) && !distinctLimits.has(limitValue)) {
|
||||
distinctLimits.add(limitValue);
|
||||
}
|
||||
}
|
||||
|
||||
return distinctLimits.size > 1;
|
||||
}
|
||||
|
||||
export function isLimitReached(limit: 'Commits'): boolean;
|
||||
export function isLimitReached(
|
||||
limit: 'Branches' | 'ConcurrentPRs',
|
||||
config: BranchConfig,
|
||||
): boolean;
|
||||
export function isLimitReached(
|
||||
limit: 'Commits' | 'Branches' | 'ConcurrentPRs',
|
||||
config?: BranchConfig,
|
||||
): boolean {
|
||||
if (limit === 'Commits') {
|
||||
return handleCommitsLimit();
|
||||
}
|
||||
|
||||
if (config) {
|
||||
return handleConcurrentLimits(limit, config);
|
||||
}
|
||||
|
||||
// istanbul ignore next: should not happen
|
||||
throw new Error(
|
||||
'Config is required for computing limits for Branches and PullRequests',
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ beforeEach(() => {
|
|||
});
|
||||
|
||||
describe('workers/repository/process/limits', () => {
|
||||
describe('getPrHourlyRemaining()', () => {
|
||||
it('calculates hourly limit remaining', async () => {
|
||||
describe('getPrHourlyCount()', () => {
|
||||
it('calculates hourly pr count', async () => {
|
||||
const time = DateTime.local();
|
||||
const createdAt = time.toISO();
|
||||
platform.getPrList.mockResolvedValueOnce([
|
||||
|
@ -33,30 +33,19 @@ describe('workers/repository/process/limits', () => {
|
|||
{ createdAt, sourceBranch: 'bar/configure' },
|
||||
{ createdAt, sourceBranch: 'baz/test' },
|
||||
] as never);
|
||||
const res = await limits.getPrHourlyRemaining({
|
||||
...config,
|
||||
prHourlyLimit: 10,
|
||||
});
|
||||
expect(res).toBe(7);
|
||||
const res = await limits.getPrHourlyCount(config);
|
||||
expect(res).toBe(3);
|
||||
});
|
||||
|
||||
it('returns prHourlyLimit if errored', async () => {
|
||||
config.prHourlyLimit = 5;
|
||||
it('returns zero if errored', async () => {
|
||||
platform.getPrList.mockRejectedValue('Unknown error');
|
||||
const res = await limits.getPrHourlyRemaining(config);
|
||||
expect(res).toBe(5);
|
||||
});
|
||||
|
||||
it('returns MAX_SAFE_INTEGER if no hourly limit', async () => {
|
||||
config.prHourlyLimit = 0;
|
||||
const res = await limits.getPrHourlyRemaining(config);
|
||||
expect(res).toBe(Number.MAX_SAFE_INTEGER);
|
||||
const res = await limits.getPrHourlyCount(config);
|
||||
expect(res).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConcurrentPrsRemaining()', () => {
|
||||
it('calculates concurrent limit remaining', async () => {
|
||||
config.prConcurrentLimit = 20;
|
||||
describe('getConcurrentPrsCount()', () => {
|
||||
it('calculates concurrent prs present', async () => {
|
||||
platform.getBranchPr.mockImplementation((branchName) =>
|
||||
branchName
|
||||
? Promise.resolve(
|
||||
|
@ -71,100 +60,21 @@ describe('workers/repository/process/limits', () => {
|
|||
{ branchName: 'test' },
|
||||
{ branchName: null },
|
||||
] as never;
|
||||
const res = await limits.getConcurrentPrsRemaining(config, branches);
|
||||
expect(res).toBe(19);
|
||||
});
|
||||
|
||||
it('returns MAX_SAFE_INTEGER if no concurrent limit', async () => {
|
||||
config.prConcurrentLimit = 0;
|
||||
const res = await limits.getConcurrentPrsRemaining(config, []);
|
||||
expect(res).toBe(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPrsRemaining()', () => {
|
||||
it('returns hourly limit', async () => {
|
||||
config.prHourlyLimit = 1;
|
||||
platform.getPrList.mockResolvedValueOnce([]);
|
||||
const res = await limits.getPrsRemaining(config, []);
|
||||
expect(res).toBe(1);
|
||||
});
|
||||
|
||||
it('returns concurrent limit', async () => {
|
||||
config.prConcurrentLimit = 1;
|
||||
const res = await limits.getPrsRemaining(config, []);
|
||||
const res = await limits.getConcurrentPrsCount(config, branches);
|
||||
expect(res).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConcurrentBranchesRemaining()', () => {
|
||||
it('calculates concurrent limit remaining', async () => {
|
||||
config.branchConcurrentLimit = 20;
|
||||
scm.branchExists.mockResolvedValueOnce(true);
|
||||
const res = await limits.getConcurrentBranchesRemaining(config, [
|
||||
{ branchName: 'foo' },
|
||||
] as never);
|
||||
expect(res).toBe(19);
|
||||
});
|
||||
|
||||
it('defaults to prConcurrentLimit', async () => {
|
||||
config.branchConcurrentLimit = null;
|
||||
config.prConcurrentLimit = 20;
|
||||
scm.branchExists.mockResolvedValueOnce(true);
|
||||
const res = await limits.getConcurrentBranchesRemaining(config, [
|
||||
{ branchName: 'foo' },
|
||||
] as never);
|
||||
expect(res).toBe(19);
|
||||
});
|
||||
|
||||
it('does not use prConcurrentLimit for explicit branchConcurrentLimit=0', async () => {
|
||||
config.branchConcurrentLimit = 0;
|
||||
config.prConcurrentLimit = 20;
|
||||
const res = await limits.getConcurrentBranchesRemaining(config, []);
|
||||
expect(res).toBe(Number.MAX_SAFE_INTEGER);
|
||||
});
|
||||
|
||||
it('returns 10 if no limits are set', async () => {
|
||||
const res = await limits.getConcurrentBranchesRemaining(config, []);
|
||||
expect(res).toBe(10);
|
||||
});
|
||||
|
||||
it('returns prConcurrentLimit if errored', async () => {
|
||||
config.branchConcurrentLimit = 2;
|
||||
// TODO: #22198
|
||||
const res = await limits.getConcurrentBranchesRemaining(
|
||||
config,
|
||||
null as never,
|
||||
describe('getConcurrentBranchesCount()', () => {
|
||||
it('calculates concurrent branches present', async () => {
|
||||
scm.branchExists.mockImplementation((branchName) =>
|
||||
branchName ? Promise.resolve(true) : Promise.resolve(false),
|
||||
);
|
||||
expect(res).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBranchesRemaining()', () => {
|
||||
it('returns minimal of both limits', async () => {
|
||||
platform.getPrList.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
limits.getBranchesRemaining(
|
||||
{
|
||||
...config,
|
||||
prHourlyLimit: 3,
|
||||
branchConcurrentLimit: 5,
|
||||
},
|
||||
[],
|
||||
),
|
||||
).resolves.toBe(3);
|
||||
|
||||
await expect(
|
||||
limits.getBranchesRemaining(
|
||||
{
|
||||
...config,
|
||||
prHourlyLimit: 11,
|
||||
branchConcurrentLimit: 7,
|
||||
},
|
||||
[],
|
||||
),
|
||||
).resolves.toBe(7);
|
||||
const res = await limits.getConcurrentBranchesCount([
|
||||
{ branchName: 'foo' },
|
||||
{ branchName: null },
|
||||
] as never);
|
||||
expect(res).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,53 +1,45 @@
|
|||
import { DateTime } from 'luxon';
|
||||
import type { RenovateConfig } from '../../../config/types';
|
||||
import { logger } from '../../../logger';
|
||||
import type { Pr } from '../../../modules/platform';
|
||||
import { platform } from '../../../modules/platform';
|
||||
import { scm } from '../../../modules/platform/scm';
|
||||
import { ExternalHostError } from '../../../types/errors/external-host-error';
|
||||
import type { BranchConfig } from '../../types';
|
||||
|
||||
export async function getPrHourlyRemaining(
|
||||
export async function getPrHourlyCount(
|
||||
config: RenovateConfig,
|
||||
): Promise<number> {
|
||||
if (config.prHourlyLimit) {
|
||||
try {
|
||||
logger.debug('Calculating hourly PRs remaining');
|
||||
const prList = await platform.getPrList();
|
||||
const currentHourStart = DateTime.local().startOf('hour');
|
||||
logger.debug(`currentHourStart=${String(currentHourStart)}`);
|
||||
const currentHourStart = DateTime.local().setZone('utc').startOf('hour');
|
||||
logger.debug(
|
||||
`Calculating PRs created so far in this hour currentHourStart=${String(currentHourStart)}`,
|
||||
);
|
||||
const soFarThisHour = prList.filter(
|
||||
(pr) =>
|
||||
pr.sourceBranch !== config.onboardingBranch &&
|
||||
pr.sourceBranch.startsWith(config.branchPrefix!) &&
|
||||
DateTime.fromISO(pr.createdAt!) > currentHourStart,
|
||||
);
|
||||
const prsRemaining = Math.max(
|
||||
0,
|
||||
config.prHourlyLimit - soFarThisHour.length,
|
||||
logger.debug(
|
||||
`${soFarThisHour.length} PRs have been created so far in this hour.`,
|
||||
);
|
||||
logger.debug(`PR hourly limit remaining: ${prsRemaining}`);
|
||||
return prsRemaining;
|
||||
return soFarThisHour.length;
|
||||
} catch (err) {
|
||||
// istanbul ignore if
|
||||
if (err instanceof ExternalHostError) {
|
||||
throw err;
|
||||
}
|
||||
logger.error({ err }, 'Error checking PRs created per hour');
|
||||
return config.prHourlyLimit;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
export async function getConcurrentPrsRemaining(
|
||||
export async function getConcurrentPrsCount(
|
||||
config: RenovateConfig,
|
||||
branches: BranchConfig[],
|
||||
): Promise<number> {
|
||||
if (config.prConcurrentLimit) {
|
||||
logger.debug(`Calculating prConcurrentLimit (${config.prConcurrentLimit})`);
|
||||
try {
|
||||
const openPrs: Pr[] = [];
|
||||
let openPrCount = 0;
|
||||
for (const { branchName } of branches) {
|
||||
try {
|
||||
const pr = await platform.getBranchPr(branchName, config.baseBranch);
|
||||
|
@ -56,7 +48,7 @@ export async function getConcurrentPrsRemaining(
|
|||
pr.sourceBranch !== config.onboardingBranch &&
|
||||
pr.state === 'open'
|
||||
) {
|
||||
openPrs.push(pr);
|
||||
openPrCount++;
|
||||
}
|
||||
} catch (err) {
|
||||
// istanbul ignore if
|
||||
|
@ -67,75 +59,21 @@ export async function getConcurrentPrsRemaining(
|
|||
}
|
||||
}
|
||||
}
|
||||
logger.debug(`${openPrs.length} PRs are currently open`);
|
||||
const concurrentRemaining = Math.max(
|
||||
0,
|
||||
config.prConcurrentLimit - openPrs.length,
|
||||
);
|
||||
logger.debug(`PR concurrent limit remaining: ${concurrentRemaining}`);
|
||||
return concurrentRemaining;
|
||||
} catch (err) /* istanbul ignore next */ {
|
||||
logger.error({ err }, 'Error checking concurrent PRs');
|
||||
return config.prConcurrentLimit;
|
||||
}
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
|
||||
logger.debug(`${openPrCount} PRs are currently open`);
|
||||
return openPrCount;
|
||||
}
|
||||
|
||||
export async function getPrsRemaining(
|
||||
config: RenovateConfig,
|
||||
export async function getConcurrentBranchesCount(
|
||||
branches: BranchConfig[],
|
||||
): Promise<number> {
|
||||
const hourlyRemaining = await getPrHourlyRemaining(config);
|
||||
const concurrentRemaining = await getConcurrentPrsRemaining(config, branches);
|
||||
return Math.min(hourlyRemaining, concurrentRemaining);
|
||||
}
|
||||
|
||||
export async function getConcurrentBranchesRemaining(
|
||||
config: RenovateConfig,
|
||||
branches: BranchConfig[],
|
||||
): Promise<number> {
|
||||
const { branchConcurrentLimit, prConcurrentLimit } = config;
|
||||
const limit =
|
||||
typeof branchConcurrentLimit === 'number'
|
||||
? branchConcurrentLimit
|
||||
: prConcurrentLimit;
|
||||
if (typeof limit === 'number' && limit) {
|
||||
logger.debug(`Calculating branchConcurrentLimit (${limit})`);
|
||||
try {
|
||||
const existingBranches: string[] = [];
|
||||
let existingBranchCount = 0;
|
||||
for (const branch of branches) {
|
||||
if (await scm.branchExists(branch.branchName)) {
|
||||
existingBranches.push(branch.branchName);
|
||||
existingBranchCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const existingCount = existingBranches.length;
|
||||
logger.debug(
|
||||
`${existingCount} already existing branches found: ${existingBranches.join()}`,
|
||||
);
|
||||
|
||||
const concurrentRemaining = Math.max(0, limit - existingCount);
|
||||
logger.debug(`Branch concurrent limit remaining: ${concurrentRemaining}`);
|
||||
|
||||
return concurrentRemaining;
|
||||
} catch (err) {
|
||||
// TODO: #22198 should never throw
|
||||
logger.error({ err }, 'Error checking concurrent branches');
|
||||
return limit;
|
||||
}
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
export async function getBranchesRemaining(
|
||||
config: RenovateConfig,
|
||||
branches: BranchConfig[],
|
||||
): Promise<number> {
|
||||
const hourlyRemaining = await getPrHourlyRemaining(config);
|
||||
const concurrentRemaining = await getConcurrentBranchesRemaining(
|
||||
config,
|
||||
branches,
|
||||
);
|
||||
return Math.min(hourlyRemaining, concurrentRemaining);
|
||||
logger.debug(`${existingBranchCount} already existing branches found.`);
|
||||
return existingBranchCount;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import type {
|
|||
} from '../../../util/cache/repository/types';
|
||||
import { fingerprint } from '../../../util/fingerprint';
|
||||
import type { LongCommitSha } from '../../../util/git/types';
|
||||
import { isLimitReached } from '../../global/limits';
|
||||
import { counts } from '../../global/limits';
|
||||
import type { BranchConfig, BranchUpgradeConfig } from '../../types';
|
||||
import * as _branchWorker from '../update/branch';
|
||||
import * as _limits from './limits';
|
||||
|
@ -32,8 +32,9 @@ const repoCache = mocked(_repoCache);
|
|||
|
||||
branchWorker.processBranch = jest.fn();
|
||||
|
||||
limits.getPrsRemaining = jest.fn().mockResolvedValue(99);
|
||||
limits.getBranchesRemaining = jest.fn().mockResolvedValue(99);
|
||||
limits.getConcurrentPrsCount = jest.fn().mockResolvedValue(0);
|
||||
limits.getConcurrentBranchesCount = jest.fn().mockResolvedValue(0);
|
||||
limits.getPrHourlyCount = jest.fn().mockResolvedValue(0);
|
||||
|
||||
let config: RenovateConfig;
|
||||
|
||||
|
@ -104,22 +105,35 @@ describe('workers/repository/process/write', () => {
|
|||
|
||||
it('increments branch counter', async () => {
|
||||
const branchName = 'branchName';
|
||||
const branches: BranchConfig[] = [
|
||||
{ baseBranch: 'main', branchName, upgrades: [], manager: 'npm' },
|
||||
{ baseBranch: 'dev', branchName, upgrades: [], manager: 'npm' },
|
||||
];
|
||||
const branches = partial<BranchConfig[]>([
|
||||
{
|
||||
baseBranch: 'main',
|
||||
branchName,
|
||||
upgrades: partial<BranchUpgradeConfig>([{ prConcurrentLimit: 10 }]),
|
||||
manager: 'npm',
|
||||
},
|
||||
{
|
||||
baseBranch: 'dev',
|
||||
branchName,
|
||||
upgrades: partial<BranchUpgradeConfig>([{ prConcurrentLimit: 10 }]),
|
||||
manager: 'npm',
|
||||
},
|
||||
]);
|
||||
repoCache.getCache.mockReturnValueOnce({});
|
||||
branchWorker.processBranch.mockResolvedValueOnce({
|
||||
branchExists: true,
|
||||
result: 'pr-created',
|
||||
});
|
||||
scm.branchExists.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
limits.getBranchesRemaining.mockResolvedValueOnce(1);
|
||||
expect(isLimitReached('Branches')).toBeFalse();
|
||||
|
||||
limits.getConcurrentPrsCount.mockResolvedValue(0);
|
||||
limits.getConcurrentBranchesCount.mockResolvedValue(0);
|
||||
limits.getPrHourlyCount.mockResolvedValue(0);
|
||||
|
||||
scm.branchExists.mockResolvedValueOnce(false).mockResolvedValue(true);
|
||||
GlobalConfig.set({ dryRun: 'full' });
|
||||
config.baseBranches = ['main', 'dev'];
|
||||
await writeUpdates(config, branches);
|
||||
expect(isLimitReached('Branches')).toBeTrue();
|
||||
expect(counts.get('Branches')).toBe(1);
|
||||
expect(addMeta).toHaveBeenCalledWith({
|
||||
baseBranch: 'main',
|
||||
branch: branchName,
|
||||
|
|
|
@ -7,11 +7,15 @@ import { getCache } from '../../../util/cache/repository';
|
|||
import type { BranchCache } from '../../../util/cache/repository/types';
|
||||
import { fingerprint } from '../../../util/fingerprint';
|
||||
import { setBranchNewCommit } from '../../../util/git/set-branch-commit';
|
||||
import { incLimitedValue, setMaxLimit } from '../../global/limits';
|
||||
import { incCountValue, setCount } from '../../global/limits';
|
||||
import type { BranchConfig, UpgradeFingerprintConfig } from '../../types';
|
||||
import { processBranch } from '../update/branch';
|
||||
import { upgradeFingerprintFields } from './fingerprint-fields';
|
||||
import { getBranchesRemaining, getPrsRemaining } from './limits';
|
||||
import {
|
||||
getConcurrentBranchesCount,
|
||||
getConcurrentPrsCount,
|
||||
getPrHourlyCount,
|
||||
} from './limits';
|
||||
|
||||
export type WriteUpdateResult = 'done' | 'automerged';
|
||||
|
||||
|
@ -127,15 +131,15 @@ export async function writeUpdates(
|
|||
.sort()
|
||||
.join(', ')}`,
|
||||
);
|
||||
const prsRemaining = await getPrsRemaining(config, branches);
|
||||
logger.debug(`Calculated maximum PRs remaining this run: ${prsRemaining}`);
|
||||
setMaxLimit('PullRequests', prsRemaining);
|
||||
|
||||
const branchesRemaining = await getBranchesRemaining(config, branches);
|
||||
logger.debug(
|
||||
`Calculated maximum branches remaining this run: ${branchesRemaining}`,
|
||||
);
|
||||
setMaxLimit('Branches', branchesRemaining);
|
||||
const concurrentPrsCount = await getConcurrentPrsCount(config, branches);
|
||||
setCount('ConcurrentPRs', concurrentPrsCount);
|
||||
|
||||
const concurrentBranchesCount = await getConcurrentBranchesCount(branches);
|
||||
setCount('Branches', concurrentBranchesCount);
|
||||
|
||||
const prsThisHourCount = await getPrHourlyCount(config);
|
||||
setCount('HourlyPRs', prsThisHourCount);
|
||||
|
||||
for (const branch of branches) {
|
||||
const { baseBranch, branchName } = branch;
|
||||
|
@ -182,7 +186,7 @@ export async function writeUpdates(
|
|||
return 'automerged';
|
||||
}
|
||||
if (!branchExisted && (await scm.branchExists(branch.branchName))) {
|
||||
incLimitedValue('Branches');
|
||||
incCountValue('Branches');
|
||||
}
|
||||
}
|
||||
removeMeta(['branch', 'baseBranch']);
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
import { coerceNumber } from '../../../../util/number';
|
||||
import { toMs } from '../../../../util/pretty-time';
|
||||
import * as template from '../../../../util/template';
|
||||
import { isLimitReached } from '../../../global/limits';
|
||||
import { getCount, isLimitReached } from '../../../global/limits';
|
||||
import type { BranchConfig, BranchResult, PrBlockedBy } from '../../../types';
|
||||
import { embedChangelogs } from '../../changelog';
|
||||
import { ensurePr, getPlatformPrOptions } from '../pr';
|
||||
|
@ -212,9 +212,14 @@ export async function processBranch(
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Open PR Count: ${getCount('ConcurrentPRs')}, Existing Branch Count: ${getCount('Branches')}, Hourly PR Count: ${getCount('HourlyPRs')}`,
|
||||
);
|
||||
|
||||
if (
|
||||
!branchExists &&
|
||||
isLimitReached('Branches') &&
|
||||
isLimitReached('Branches', branchConfig) &&
|
||||
!dependencyDashboardCheck &&
|
||||
!config.isVulnerabilityAlert
|
||||
) {
|
||||
|
|
|
@ -89,8 +89,9 @@ describe('workers/repository/update/pr/index', () => {
|
|||
const res = await ensurePr(config);
|
||||
|
||||
expect(res).toEqual({ type: 'with-pr', pr });
|
||||
expect(limits.incLimitedValue).toHaveBeenCalledOnce();
|
||||
expect(limits.incLimitedValue).toHaveBeenCalledWith('PullRequests');
|
||||
expect(limits.incCountValue).toHaveBeenCalledTimes(2);
|
||||
expect(limits.incCountValue).toHaveBeenCalledWith('ConcurrentPRs');
|
||||
expect(limits.incCountValue).toHaveBeenCalledWith('HourlyPRs');
|
||||
expect(logger.logger.info).toHaveBeenCalledWith(
|
||||
{ pr: pr.number, prTitle },
|
||||
'PR created',
|
||||
|
|
|
@ -27,7 +27,7 @@ import { stripEmojis } from '../../../../util/emoji';
|
|||
import { fingerprint } from '../../../../util/fingerprint';
|
||||
import { getBranchLastCommitTime } from '../../../../util/git';
|
||||
import { memoize } from '../../../../util/memoize';
|
||||
import { incLimitedValue, isLimitReached } from '../../../global/limits';
|
||||
import { incCountValue, isLimitReached } from '../../../global/limits';
|
||||
import type {
|
||||
BranchConfig,
|
||||
BranchUpgradeConfig,
|
||||
|
@ -482,7 +482,7 @@ export async function ensurePr(
|
|||
try {
|
||||
if (
|
||||
!dependencyDashboardCheck &&
|
||||
isLimitReached('PullRequests') &&
|
||||
isLimitReached('ConcurrentPRs', prConfig) &&
|
||||
!config.isVulnerabilityAlert
|
||||
) {
|
||||
logger.debug('Skipping PR - limit reached');
|
||||
|
@ -499,7 +499,8 @@ export async function ensurePr(
|
|||
milestone: config.milestone,
|
||||
});
|
||||
|
||||
incLimitedValue('PullRequests');
|
||||
incCountValue('ConcurrentPRs');
|
||||
incCountValue('HourlyPRs');
|
||||
logger.info({ pr: pr?.number, prTitle }, 'PR created');
|
||||
} catch (err) {
|
||||
logger.debug({ err }, 'Pull request creation error');
|
||||
|
|
Loading…
Reference in a new issue