const { DateTime } = require('luxon'); const schedule = require('./schedule'); const { getUpdatedPackageFiles } = require('./get-updated'); const { getAdditionalFiles } = require('../../manager/npm/post-update'); const { commitFilesToBranch } = require('./commit'); const { getParentBranch } = require('./parent'); const { tryBranchAutomerge } = require('./automerge'); const { setUnpublishable } = require('./status-checks'); const { prAlreadyExisted } = require('./check-existing'); const prWorker = require('../pr'); const { isScheduledNow } = schedule; module.exports = { processBranch, }; async function processBranch(branchConfig, packageFiles) { logger.debug(`processBranch with ${branchConfig.upgrades.length} upgrades`); const config = { ...branchConfig }; const dependencies = config.upgrades .map(upgrade => upgrade.depName) .filter(v => v) // remove nulls (happens for lock file maintenance) .filter((value, i, list) => list.indexOf(value) === i); // remove duplicates logger.setMeta({ repository: config.repository, branch: config.branchName, dependencies, }); logger.debug('processBranch()'); logger.trace({ config }); await platform.setBaseBranch(config.baseBranch); const branchExists = await platform.branchExists(config.branchName); logger.debug(`branchExists=${branchExists}`); if (!branchExists && config.prHourlyLimitReached) { logger.info('Reached PR creation limit - skipping branch creation'); return 'pr-hourly-limit-reached'; } try { logger.debug( `Branch has ${dependencies ? dependencies.length : 0} upgrade(s)` ); // Check if branch already existed const existingPr = await prAlreadyExisted(config); if (existingPr) { logger.debug( { prTitle: config.prTitle }, 'Closed PR already exists. Skipping branch.' ); if (existingPr.state === 'closed') { const subject = 'Renovate Ignore Notification'; let content; if (config.updateType === 'major') { content = `As this PR has been closed unmerged, Renovate will ignore this upgrade and you will not receive PRs for *any* future ${ config.newMajor }.x releases. However, if you upgrade to ${ config.newMajor }.x manually then Renovate will then reenable updates for minor and patch updates automatically.`; } else if (config.updateType === 'digest') { content = `As this PR has been closed unmerged, Renovate will ignore this upgrade updateType and you will not receive PRs for *any* future ${ config.depName }:${ config.currentTag } digest updates. Digest updates will resume if you update the specified tag at any time.`; } else { content = `As this PR has been closed unmerged, Renovate will now ignore this update (${ config.newValue }). You will still receive a PR once a newer version is released, so if you wish to permanently ignore this dependency, please add it to the \`ignoreDeps\` array of your renovate config.`; } content += '\n\nIf this PR was closed by mistake or you changed your mind, you can simply rename this PR and you will soon get a fresh replacement PR opened.'; await platform.ensureComment(existingPr.number, subject, content); if (branchExists) { await platform.deleteBranch(config.branchName); } } else if (existingPr.state === 'merged') { logger.info( { pr: existingPr.number }, 'Merged PR is blocking this branch' ); } return 'already-existed'; } let branchPr; if (branchExists) { logger.debug('Checking if PR has been edited'); branchPr = await platform.getBranchPr(config.branchName); if (branchPr) { logger.debug('Found existing branch PR'); if (branchPr.state !== 'open') { logger.info( 'PR has been closed or merged since this run started - aborting' ); throw new Error('repository-changed'); } if (!branchPr.canRebase) { const subject = 'PR has been edited'; if (branchPr.labels && branchPr.labels.includes(config.rebaseLabel)) { await platform.ensureCommentRemoval(branchPr.number, subject); } else { let content = ':construction_worker: This PR has received other commits, so Renovate will stop updating it to avoid conflicts or other problems.'; content += ` If you wish to abandon your changes and have Renovate start over then you can add the label \`${ config.rebaseLabel }\` to this PR and Renovate will reset/recreate it.`; await platform.ensureComment(branchPr.number, subject, content); return 'pr-edited'; } } } } // Check schedule config.isScheduledNow = isScheduledNow(config); if (!config.isScheduledNow) { if (!branchExists) { logger.info('Skipping branch creation as not within schedule'); return 'not-scheduled'; } if (config.updateNotScheduled === false) { logger.debug('Skipping branch update as not within schedule'); return 'not-scheduled'; } // istanbul ignore if if (!branchPr) { logger.debug('Skipping PR creation out of schedule'); return 'not-scheduled'; } logger.debug( 'Branch + PR exists but is not scheduled -- will update if necessary' ); } if ( config.updateType !== 'lockFileMaintenance' && config.unpublishSafe && config.canBeUnpublished && (config.prCreation === 'not-pending' || config.prCreation === 'status-success') ) { logger.info( 'Skipping branch creation due to unpublishSafe + status checks' ); return 'pending'; } Object.assign(config, await getParentBranch(config)); logger.debug(`Using parentBranch: ${config.parentBranch}`); Object.assign(config, await getUpdatedPackageFiles(config)); if (config.updatedPackageFiles && config.updatedPackageFiles.length) { logger.debug( `Updated ${config.updatedPackageFiles.length} package files` ); } else { logger.debug('No package files need updating'); } const additionalFiles = await getAdditionalFiles(config, packageFiles); config.lockFileErrors = additionalFiles.lockFileErrors; config.updatedLockFiles = (config.updatedLockFiles || []).concat( additionalFiles.updatedLockFiles ); if (config.updatedLockFiles && config.updatedLockFiles.length) { logger.debug( { updatedLockFiles: config.updatedLockFiles.map(f => f.name) }, `Updated ${config.updatedLockFiles.length} lock files` ); } else { logger.debug('No updated lock files in branch'); } if (config.lockFileErrors && config.lockFileErrors.length) { if (config.releaseTimestamp) { logger.debug(`Branch timestamp: ` + config.releaseTimestamp); const releaseTimestamp = DateTime.fromISO(config.releaseTimestamp); if (releaseTimestamp.plus({ days: 1 }) < DateTime.local()) { logger.info('PR is older than a day, raise PR with lock file errors'); } else if (branchExists) { logger.info( 'PR is less than a day old but branchExists so updating anyway' ); } else { logger.info('PR is less than a day old - raise error instead of PR'); throw new Error('lockfile-error'); } } else { logger.debug('PR has no releaseTimestamp'); } } config.committedFiles = await commitFilesToBranch(config); // istanbul ignore if if ( config.updateType === 'lockFileMaintenance' && !config.committedFiles && !config.parentBranch && branchExists ) { logger.info( 'Deleting lock file maintenance branch as master lock file no longer needs updating' ); await platform.deleteBranch(config.branchName); return 'done'; } if (!(config.committedFiles || branchExists)) { return 'no-work'; } // Set branch statuses await setUnpublishable(config); // Try to automerge branch and finish if successful, but only if branch already existed before this run if (branchExists || !config.requiresStatusChecks) { const mergeStatus = await tryBranchAutomerge(config); logger.debug(`mergeStatus=${mergeStatus}`); if (mergeStatus === 'automerged') { logger.debug('Branch is automerged - returning'); return 'automerged'; } if ( mergeStatus === 'automerge aborted - PR exists' || mergeStatus === 'branch status error' || mergeStatus === 'failed' ) { logger.info({ mergeStatus }, 'Branch automerge not possible'); config.forcePr = true; config.branchAutomergeFailureMessage = mergeStatus; } } } catch (err) /* istanbul ignore next */ { if (err.message === 'rate-limit-exceeded') { logger.debug('Passing rate-limit-exceeded error up'); throw err; } if (err.message === 'repository-changed') { logger.debug('Passing repository-changed error up'); throw err; } if (err.message === 'bad-credentials') { logger.debug('Passing bad-credentials error up'); throw err; } if (err.message === 'lockfile-error') { logger.debug('Passing lockfile-error up'); throw err; } if (err.message.startsWith('disk-space')) { logger.debug('Passing disk-space error up'); throw err; } if ( err.message !== 'registry-failure' && err.message !== 'platform-failure' ) { logger.error({ err }, `Error updating branch: ${err.message}`); } // Don't throw here - we don't want to stop the other renovations return 'error'; } try { logger.debug('Ensuring PR'); logger.debug( `There are ${config.errors.length} errors and ${ config.warnings.length } warnings` ); const pr = await prWorker.ensurePr(config); // TODO: ensurePr should check for automerge itself if (pr) { const topic = ':warning: Lock file problem'; if (config.lockFileErrors && config.lockFileErrors.length) { logger.warn( { lockFileErrors: config.lockFileErrors }, 'lockFileErrors' ); let content = `Renovate failed when attempting to generate `; content += config.lockFileErrors.length > 1 ? 'lock files' : 'a lock file'; content += '. The most frequent cause is when you have private modules but have not added configuration for [private module support](https://renovatebot.com/docs/private-modules/) but sometimes it can be just a temporary fault. It is strongly recommended that you do not merge this PR as-is.'; content += '\n\nRenovate **will not retry** generating a lockfile for this PR unless either (a) the `package.json` in this branch needs updating, (b) the branch becomes conflicted, or (c) '; if (config.recreateClosed) { content += 'you manually delete this PR so that it can be regenerated.'; } else { content += 'you rename then delete this PR unmerged, so that it can be regenerated.'; } content += `\n\n**To trigger a manual retry of this branch, add the label \`${ config.rebaseLabel }\` to this PR.**\n\n`; content += '\n\nThe lock file failure details are included below:\n\n'; config.lockFileErrors.forEach(error => { content += `##### ${error.lockFile}\n\n`; content += `\`\`\`\n${error.stderr}\n\`\`\`\n\n`; }); await platform.ensureComment(pr.number, topic, content); const context = 'renovate/lock-files'; const description = 'Lock file update failure'; const state = 'failure'; const existingState = await platform.getBranchStatusCheck( config.branchName, context ); // Check if state needs setting if (existingState !== state) { logger.debug(`Updating status check state to failed`); await platform.setBranchStatus( config.branchName, context, description, state ); } } else { if (config.updatedLockFiles && config.updatedLockFiles.length) { await platform.ensureCommentRemoval(pr.number, topic); } const prAutomerged = await prWorker.checkAutoMerge(pr, config); if (prAutomerged) { return 'automerged'; } } } } catch (err) /* istanbul ignore next */ { if (['rate-limit-exceeded', 'platform-failure'].includes(err.message)) { logger.debug('Passing PR error up'); throw err; } // Otherwise don't throw here - we don't want to stop the other renovations logger.error({ err }, `Error ensuring PR: ${err.message}`); } if (!branchExists) { return 'pr-created'; } return 'done'; }