diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 5ba22170f99294a6cd8cedef3eadaed2744d5b27..f4e304981d1b145bee7b48263f0b87159ddc604e 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -83,6 +83,10 @@ Set this to true if you wish for Renovate to persist repo data between runs. The ## platform +## prCommitsPerRunLimit + +Parameter to reduce CI load. CI jobs are usually triggered by these events: pull-request creation, pull-request update, automerge events. Set as an integer. Default is no limit. + ## prFooter ## printConfig diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts index abe48135816f6a08f4ad40e74f86cdaea66ea8f0..33b9b7966cc0dc676c862266c7aafbf39c452d9c 100644 --- a/lib/config/definitions.ts +++ b/lib/config/definitions.ts @@ -447,6 +447,14 @@ const options: RenovateOptions[] = [ type: 'string', default: null, }, + { + name: 'prCommitsPerRunLimit', + description: + 'Set a maximum number of commits per Renovate run. Default is no limit.', + stage: 'global', + type: 'integer', + default: 0, + }, { name: 'repositories', description: 'List of Repositories', diff --git a/lib/platform/git/storage.ts b/lib/platform/git/storage.ts index 2ccd3375245ceb4fbd7e37dab6e6518a10ccb9d1..bfe7bf9fa3a0c65ec366ab1606721575430fa5cf 100644 --- a/lib/platform/git/storage.ts +++ b/lib/platform/git/storage.ts @@ -5,6 +5,8 @@ import Git from 'simple-git/promise'; import URL from 'url'; import { logger } from '../../logger'; +const limits = require('../../workers/global/limits'); + declare module 'fs-extra' { // eslint-disable-next-line import/prefer-default-export export function exists(pathLike: string): Promise<boolean>; @@ -366,6 +368,7 @@ export class Storage { await this._git!.checkout(this._config.baseBranch); await this._git!.merge(['--ff-only', branchName]); await this._git!.push('origin', this._config.baseBranch); + limits.incrementLimit('prCommitsPerRunLimit'); } async getBranchLastCommitTime(branchName: string) { @@ -450,6 +453,7 @@ export class Storage { const ref = `refs/heads/${branchName}:refs/remotes/origin/${branchName}`; await this._git!.fetch(['origin', ref, '--depth=2', '--force']); this._config.branchExists[branchName] = true; + limits.incrementLimit('prCommitsPerRunLimit'); } catch (err) /* istanbul ignore next */ { checkForPlatformFailure(err); logger.debug({ err }, 'Error commiting files'); diff --git a/lib/workers/branch/index.js b/lib/workers/branch/index.js index c7ec27623498dcf0844868464db3143719f9ba15..e6afd3d4a60eb837b69236e3caea1ac8f96aebfb 100644 --- a/lib/workers/branch/index.js +++ b/lib/workers/branch/index.js @@ -111,7 +111,9 @@ async function processBranch(branchConfig, prHourlyLimitReached, packageFiles) { !masterIssueCheck && !config.vulnerabilityAlert ) { - logger.info('Reached PR creation limit - skipping branch creation'); + logger.info( + 'Reached PR creation limit or per run commits limit - skipping branch creation' + ); return 'pr-hourly-limit-reached'; } if (branchExists) { diff --git a/lib/workers/global/index.js b/lib/workers/global/index.js index a5564566cc7a149d4807de213e32f40c7301d4ba..a17fc66233adce7a5f32e1633197f37291383cbb 100644 --- a/lib/workers/global/index.js +++ b/lib/workers/global/index.js @@ -13,6 +13,7 @@ const { autodiscoverRepositories } = require('./autodiscover'); const { initPlatform } = require('../../platform'); const hostRules = require('../../util/host-rules'); const { printStats } = require('../../util/got/stats'); +const limits = require('./limits'); export { start, getRepositoryConfig }; @@ -39,9 +40,16 @@ async function start() { global.trustLevel = config.trustLevel || 'low'; delete config.trustLevel; detectRenovateVersion(); + limits.init(config); setEmojiConfig(config); // Iterate through repositories sequentially for (const repository of config.repositories) { + if (limits.getLimitRemaining('prCommitsPerRunLimit') <= 0) { + logger.info( + 'Max commits created for this run. Skipping all remaining repositories.' + ); + break; + } const repoConfig = await getRepositoryConfig(config, repository); if (repoConfig.hostRules) { hostRules.clear(); diff --git a/lib/workers/global/limits.ts b/lib/workers/global/limits.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e97cf8d719a966ce3d389222a0dafd4a91e49e7 --- /dev/null +++ b/lib/workers/global/limits.ts @@ -0,0 +1,41 @@ +import { logger } from '../../logger'; + +const limitsToInit = ['prCommitsPerRunLimit']; +const l: Record<string, number> = {}; +const v: Record<string, number> = {}; + +export function init(config: Record<string, any>) { + logger.info(`Limits.init enter method`); + for (const limit of limitsToInit) { + logger.info(`Limits.init ${limit} processing`); + if (config[limit]) { + setLimit(limit, config[limit]); + v[limit] = 0; + } else { + logger.info( + `Limits.init ${limit} variable is not set. Ignoring ${limit}` + ); + } + } +} + +export function setLimit(name: string, value: number) { + logger.debug(`Limits.setLimit l[${name}] = ${value}`); + l[name] = value; +} + +export function getLimitRemaining(name: string) { + let result; + if (typeof v[name] !== 'undefined') { + result = l[name] - v[name]; + } else { + result = undefined; + } + return result; +} + +export function incrementLimit(name: string, value = 1) { + if (typeof v[name] !== 'undefined') { + v[name] += value; + } +} diff --git a/lib/workers/repository/process/write.js b/lib/workers/repository/process/write.js index 4c0daa1aa66b12238c60c8267ea962add9f77229..68857248d1f3bdb067075f2b96296f38bdfc0f02 100644 --- a/lib/workers/repository/process/write.js +++ b/lib/workers/repository/process/write.js @@ -1,6 +1,7 @@ const { logger } = require('../../../logger'); const branchWorker = require('../../branch'); const { getPrsRemaining } = require('./limits'); +const limits = require('../../global/limits'); module.exports = { writeUpdates, @@ -27,7 +28,8 @@ async function writeUpdates(config, packageFiles, allBranches) { for (const branch of branches) { const res = await branchWorker.processBranch( branch, - prsRemaining <= 0, + prsRemaining <= 0 || + limits.getLimitRemaining('prCommitsPerRunLimit') <= 0, packageFiles ); branch.res = res; diff --git a/renovate-schema.json b/renovate-schema.json index 5cce87fa10f4c609038a8abe31ada3db4860ffbb..dac8e9819111e97958b61c8a6c01b3051327719b 100644 --- a/renovate-schema.json +++ b/renovate-schema.json @@ -235,6 +235,11 @@ "type": "string", "default": null }, + "prCommitsPerRunLimit": { + "description": "Set a maximum number of commits per Renovate run. Default is no limit.", + "type": "integer", + "default": 0 + }, "repositories": { "description": "List of Repositories", "type": "array" diff --git a/test/workers/global/index.spec.js b/test/workers/global/index.spec.js index 27f5212f04c0aee8614d32d84c6b476c4bd92e09..7e365768d5ba3bc23a6201a5ada84e56e68731eb 100644 --- a/test/workers/global/index.spec.js +++ b/test/workers/global/index.spec.js @@ -7,6 +7,8 @@ const platform = require('../../../lib/platform'); jest.mock('../../../lib/platform'); +const limits = require('../../../lib/workers/global/limits'); + describe('lib/workers/global', () => { beforeEach(() => { jest.resetAllMocks(); @@ -50,6 +52,27 @@ describe('lib/workers/global', () => { expect(repositoryWorker.renovateRepository).toHaveBeenCalledTimes(2); }); + it('processes repositories break', async () => { + limits.getLimitRemaining = jest.fn(() => 0); + // limits.getLimitRemaining.mockReturnValueOnce(0); + configParser.parseConfigs.mockReturnValueOnce({ + gitAuthor: 'a@b.com', + enabled: true, + repositories: ['a', 'b'], + hostRules: [ + { + hostType: 'docker', + host: 'docker.io', + username: 'some-user', + password: 'some-password', + }, + ], + }); + await globalWorker.start(); + expect(configParser.parseConfigs).toHaveBeenCalledTimes(1); + expect(repositoryWorker.renovateRepository).toHaveBeenCalledTimes(0); + }); + describe('processes platforms', () => { it('github', async () => { configParser.parseConfigs.mockReturnValueOnce({ diff --git a/test/workers/global/limits.spec.ts b/test/workers/global/limits.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..df1b5d2ba52c0ecc8b19207a3249ec372e43bd57 --- /dev/null +++ b/test/workers/global/limits.spec.ts @@ -0,0 +1,25 @@ +import { + init, + getLimitRemaining, + incrementLimit, +} from '../../../lib/workers/global/limits'; + +describe('lib/workers/global/limits', () => { + describe('init()', () => { + it('check defined variables have a value set to zero', async () => { + const config = { prCommitsPerRunLimit: 3 }; + init(config); + const result = await getLimitRemaining('prCommitsPerRunLimit'); + expect(result).toEqual(3); + }); + }); + describe('incrementLimit()', () => { + it('check increment works as expected', async () => { + const config = { prCommitsPerRunLimit: 3 }; + init(config); + incrementLimit('prCommitsPerRunLimit', 2); + const result = await getLimitRemaining('prCommitsPerRunLimit'); + expect(result).toEqual(1); + }); + }); +}); diff --git a/test/workers/repository/process/write.spec.js b/test/workers/repository/process/write.spec.js index 3ea14619a5f1cd9dce89e1a7e6b4d3055cbc1f95..0a40870c9227f1ed26de091e5de5bfcc210274ab 100644 --- a/test/workers/repository/process/write.spec.js +++ b/test/workers/repository/process/write.spec.js @@ -5,8 +5,10 @@ const { const branchWorker = require('../../../../lib/workers/branch'); /** @type any */ const limits = require('../../../../lib/workers/repository/process/limits'); +/** @type any */ branchWorker.processBranch = jest.fn(); + limits.getPrsRemaining = jest.fn(() => 99); let config;