From 6e9c73e35a1447af27bbe6d61e3c1d8c1d07f1c4 Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@keylocation.sg> Date: Mon, 28 Aug 2017 15:50:11 +0200 Subject: [PATCH] feat: restart repo renovate after automerge (#751) Previously, the automerge feature was causing some undesirable behaviour when multiple branches were open at the same time. Example: #707. The main problem is that other branches will still be calculated based on the original `package.json` contents and not the post-merge contents. The simplest solution seems to be: - Stop all subsequent branch processing after any automerge - Restart repository renovation This continues until no branch has automerged in a cycle. Closes #750 --- lib/workers/branch/index.js | 19 +-- lib/workers/pr/index.js | 13 +- lib/workers/repository/index.js | 176 ++++++++++++++------------ test/workers/branch/index.spec.js | 1 + test/workers/repository/index.spec.js | 24 +++- 5 files changed, 139 insertions(+), 94 deletions(-) diff --git a/lib/workers/branch/index.js b/lib/workers/branch/index.js index b65b85e198..fce07cf9b4 100644 --- a/lib/workers/branch/index.js +++ b/lib/workers/branch/index.js @@ -30,14 +30,14 @@ async function processBranch(branchConfig) { // Check schedule if (!isScheduledNow(config)) { logger.info('Skipping branch as it is not scheduled'); - return; + return 'not-scheduled'; } logger.info(`Branch has ${dependencies.length} upgrade(s)`); if (await prAlreadyExisted(config)) { logger.info('Closed PR already exists. Skipping branch.'); - return; + return 'already-existed'; } config.parentBranch = await getParentBranch(config); logger.debug(`Using parentBranch: ${config.parentBranch}`); @@ -67,7 +67,7 @@ async function processBranch(branchConfig) { // Return now if no branch exists if ((await config.api.branchExists(config.branchName)) === false) { logger.debug('Branch does not exist - returning'); - return; + return 'no-branch'; } // Set branch statuses @@ -78,7 +78,7 @@ async function processBranch(branchConfig) { const branchMerged = await tryBranchAutomerge(config); if (branchMerged) { logger.debug('Branch is automerged - returning'); - return; + return 'automerged'; } } catch (err) { if (err.message !== 'lockFileError') { @@ -87,7 +87,7 @@ async function processBranch(branchConfig) { logger.info('Error updating branch'); } // Don't throw here - we don't want to stop the other renovations - return; + return 'error'; } try { logger.debug('Ensuring PR'); @@ -98,11 +98,14 @@ async function processBranch(branchConfig) { const pr = await prWorker.ensurePr(config); // TODO: ensurePr should check for automerge itself if (pr) { - await prWorker.checkAutoMerge(pr, config); + const prAutomerged = await prWorker.checkAutoMerge(pr, config); + if (prAutomerged) { + return 'automerged'; + } } } catch (err) { logger.error({ err }, `Error ensuring PR: ${err.message}`); + // Don't throw here - we don't want to stop the other renovations } - - // Don't throw here - we don't want to stop the other renovations + return 'done'; } diff --git a/lib/workers/pr/index.js b/lib/workers/pr/index.js index 63879f19c3..eea336d0ef 100644 --- a/lib/workers/pr/index.js +++ b/lib/workers/pr/index.js @@ -207,11 +207,11 @@ async function checkAutoMerge(pr, config) { // Return if PR not ready for automerge if (pr.mergeable !== true) { logger.info('PR is not mergeable'); - return; + return false; } if (config.requiredStatusChecks && pr.mergeable_state === 'unstable') { logger.info('PR mergeable state is unstable'); - return; + return false; } // Check branch status const branchStatus = await config.api.getBranchStatus( @@ -221,17 +221,18 @@ async function checkAutoMerge(pr, config) { logger.debug(`branchStatus=${branchStatus}`); if (branchStatus !== 'success') { logger.info('Branch status is not "success"'); - return; + return false; } // Check if it's been touched if (!pr.canRebase) { logger.info('PR is ready for automerge but has been modified'); - return; + return false; } // Let's merge this logger.info(`Automerging #${pr.number}`); await config.api.mergePr(pr); - } else { - logger.debug('No automerge'); + return true; } + logger.debug('No automerge'); + return false; } diff --git a/lib/workers/repository/index.js b/lib/workers/repository/index.js index 23c08f0433..fd603979b4 100644 --- a/lib/workers/repository/index.js +++ b/lib/workers/repository/index.js @@ -20,67 +20,29 @@ async function renovateRepository(repoConfig, token) { config.warnings = []; logger.trace({ config }, 'renovateRepository'); try { - config = await apis.initApis(config, token); - config = await apis.mergeRenovateJson(config); - if (config.enabled === false) { - logger.debug('repository is disabled'); - await cleanup.pruneStaleBranches(config, []); - return; - } - if (config.isFork && !config.renovateJsonPresent) { - logger.debug('repository is a fork and not manually configured'); - return; - } - if (config.baseBranch) { - // Renovate should read content and target PRs here - if (await config.api.branchExists(config.baseBranch)) { - config.api.setBaseBranch(config.baseBranch); - } else { - // Warn and ignore setting (use default branch) - const message = `The configured baseBranch "${config.baseBranch}" is not present. Ignoring`; - config.errors.push({ - depName: 'baseBranch', - message, - }); - logger.warn(message); - } - } - // Detect package files in default branch if not manually provisioned - if (config.packageFiles.length === 0) { - logger.debug('Detecting package files'); - config = await apis.detectPackageFiles(config); - // If we can't detect any package.json then return - if (config.packageFiles.length === 0) { - logger.info('Cannot detect package.json'); + let branchList; + let baseBranchUpdated; + let loopCount = 1; + do { + logger.debug(`renovateRepository loop ${loopCount}`); + baseBranchUpdated = false; + config = await apis.initApis(config, token); + config = await apis.mergeRenovateJson(config); + if (config.enabled === false) { + logger.debug('repository is disabled'); + await cleanup.pruneStaleBranches(config, []); return; } - logger.debug( - `Detected ${config.packageFiles - .length} package files: ${config.packageFiles}` - ); - } - logger.debug('Resolving package files and content'); - config = await apis.resolvePackageFiles(config); - logger.trace({ config }, 'post-packageFiles config'); - // TODO: why is this fix needed?! - config.logger = logger; - config.repoIsOnboarded = await onboarding.getOnboardingStatus(config); - if (!config.repoIsOnboarded) { - config.contentBaseBranch = `${config.branchPrefix}configure`; - // Remove packageFile list in case they are provisioned in renovate.json - const packageFiles = config.packageFiles.map( - packageFile => packageFile.packageFile - ); - config.packageFiles = []; - config = await apis.mergeRenovateJson(config, config.contentBaseBranch); - // Restore previous packageFile list if not provisioned manually - if (config.packageFiles.length === 0) { - config.packageFiles = packageFiles; + if (config.isFork && !config.renovateJsonPresent) { + logger.debug('repository is a fork and not manually configured'); + return; } if (config.baseBranch) { + // Renovate should read content and target PRs here if (await config.api.branchExists(config.baseBranch)) { - config.contentBaseBranch = config.baseBranch; + config.api.setBaseBranch(config.baseBranch); } else { + // Warn and ignore setting (use default branch) const message = `The configured baseBranch "${config.baseBranch}" is not present. Ignoring`; config.errors.push({ depName: 'baseBranch', @@ -89,34 +51,90 @@ async function renovateRepository(repoConfig, token) { logger.warn(message); } } + // Detect package files in default branch if not manually provisioned + if (config.packageFiles.length === 0) { + logger.debug('Detecting package files'); + config = await apis.detectPackageFiles(config); + // If we can't detect any package.json then return + if (config.packageFiles.length === 0) { + logger.info('Cannot detect package.json'); + return; + } + logger.debug( + `Detected ${config.packageFiles + .length} package files: ${config.packageFiles}` + ); + } + logger.debug('Resolving package files and content'); config = await apis.resolvePackageFiles(config); - config = await presets.resolveConfigPresets(config); + logger.trace({ config }, 'post-packageFiles config'); + // TODO: why is this fix needed?! config.logger = logger; - logger.trace({ config }, 'onboarding config'); - } - const allUpgrades = await upgrades.determineRepoUpgrades(config); - const res = await upgrades.branchifyUpgrades(allUpgrades, logger); - config.errors = config.errors.concat(res.errors); - config.warnings = config.warnings.concat(res.warnings); - const branchUpgrades = res.upgrades; - logger.debug(`Updating ${branchUpgrades.length} branch(es)`); - logger.trace({ config: branchUpgrades }, 'branchUpgrades'); - let branchList; - if (config.repoIsOnboarded) { - for (const branchUpgrade of branchUpgrades) { - await branchWorker.processBranch( - branchUpgrade, - config.errors, - config.warnings + config.repoIsOnboarded = await onboarding.getOnboardingStatus(config); + if (!config.repoIsOnboarded) { + config.contentBaseBranch = `${config.branchPrefix}configure`; + // Remove packageFile list in case they are provisioned in renovate.json + const packageFiles = config.packageFiles.map( + packageFile => packageFile.packageFile ); + config.packageFiles = []; + config = await apis.mergeRenovateJson(config, config.contentBaseBranch); + // Restore previous packageFile list if not provisioned manually + if (config.packageFiles.length === 0) { + config.packageFiles = packageFiles; + } + if (config.baseBranch) { + if (await config.api.branchExists(config.baseBranch)) { + config.contentBaseBranch = config.baseBranch; + } else { + const message = `The configured baseBranch "${config.baseBranch}" is not present. Ignoring`; + config.errors.push({ + depName: 'baseBranch', + message, + }); + logger.warn(message); + } + } + config = await apis.resolvePackageFiles(config); + config = await presets.resolveConfigPresets(config); + config.logger = logger; + logger.trace({ config }, 'onboarding config'); } - branchList = branchUpgrades.map(upgrade => upgrade.branchName); - logger.debug(`branchList=${branchList}`); - } else { - await onboarding.ensurePr(config, branchUpgrades); - logger.info('"Configure Renovate" PR needs to be closed first'); - branchList = [`${config.branchPrefix}configure`]; - } + const allUpgrades = await upgrades.determineRepoUpgrades(config); + const res = await upgrades.branchifyUpgrades(allUpgrades, logger); + config.errors = config.errors.concat(res.errors); + config.warnings = config.warnings.concat(res.warnings); + const branchUpgrades = res.upgrades; + logger.debug(`Updating ${branchUpgrades.length} branch(es)`); + logger.trace({ config: branchUpgrades }, 'branchUpgrades'); + if (config.repoIsOnboarded) { + for (const branchUpgrade of branchUpgrades) { + if (!baseBranchUpdated) { + const branchResult = await branchWorker.processBranch( + branchUpgrade, + config.errors, + config.warnings + ); + if (branchResult === 'automerged') { + // Stop procesing other branches because base branch has been changed by an automerge + logger.info('Restarting repo renovation after automerge'); + baseBranchUpdated = true; + } + } else { + logger.debug( + `Skipping branchUpgrade as base branch has been modified` + ); + } + } + branchList = branchUpgrades.map(upgrade => upgrade.branchName); + logger.debug(`branchList=${branchList}`); + } else { + await onboarding.ensurePr(config, branchUpgrades); + logger.info('"Configure Renovate" PR needs to be closed first'); + branchList = [`${config.branchPrefix}configure`]; + } + loopCount += 1; + } while (baseBranchUpdated); await cleanup.pruneStaleBranches(config, branchList); } catch (err) { // Swallow this error so that other repositories can be processed diff --git a/test/workers/branch/index.spec.js b/test/workers/branch/index.spec.js index 40d9f70f84..b1d8d10bc1 100644 --- a/test/workers/branch/index.spec.js +++ b/test/workers/branch/index.spec.js @@ -79,6 +79,7 @@ describe('workers/branch', () => { config.api.branchExists.mockReturnValueOnce(true); automerge.tryBranchAutomerge.mockReturnValueOnce(false); prWorker.ensurePr.mockReturnValueOnce({}); + prWorker.checkAutoMerge.mockReturnValueOnce(true); await branchWorker.processBranch(config); expect(prWorker.ensurePr.mock.calls).toHaveLength(1); expect(prWorker.checkAutoMerge.mock.calls).toHaveLength(1); diff --git a/test/workers/repository/index.spec.js b/test/workers/repository/index.spec.js index c452d92fcc..eeeb26b130 100644 --- a/test/workers/repository/index.spec.js +++ b/test/workers/repository/index.spec.js @@ -20,7 +20,7 @@ describe('workers/repository', () => { onboarding.ensurePr = jest.fn(); upgrades.determineRepoUpgrades = jest.fn(() => []); upgrades.branchifyUpgrades = jest.fn(() => ({ branchUpgrades: {} })); - branchWorker.processBranch = jest.fn(() => 'some-branch'); + branchWorker.processBranch = jest.fn(() => 'done'); config = { lockFileMaintenance: true, api: { @@ -153,6 +153,28 @@ describe('workers/repository', () => { expect(branchWorker.processBranch.mock.calls.length).toBe(3); expect(config.logger.error.mock.calls.length).toBe(0); }); + it('skips branchWorker after automerging', async () => { + config.packageFiles = ['package.json']; + config.hasRenovateJson = true; + onboarding.getOnboardingStatus.mockReturnValue(true); + upgrades.branchifyUpgrades.mockReturnValueOnce({ + upgrades: [{}, {}, {}], + }); + upgrades.branchifyUpgrades.mockReturnValueOnce({ + upgrades: [{}, {}], + }); + upgrades.branchifyUpgrades.mockReturnValueOnce({ + upgrades: [{}], + }); + upgrades.branchifyUpgrades.mockReturnValueOnce({ + upgrades: [], + }); + branchWorker.processBranch.mockReturnValue('automerged'); + await repositoryWorker.renovateRepository(config); + expect(upgrades.branchifyUpgrades.mock.calls).toHaveLength(4); + expect(branchWorker.processBranch.mock.calls).toHaveLength(3); + expect(config.logger.error.mock.calls).toHaveLength(0); + }); it('swallows errors', async () => { apis.initApis.mockImplementationOnce(() => { throw new Error('bad init'); -- GitLab