From 10622a1811d12dd59b51b3f1c42457068a97a788 Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@keylocation.sg> Date: Thu, 22 Jun 2017 09:03:36 +0200 Subject: [PATCH] Refactor to enable log context (#331) Closes #317 * Install traverse * Scrub api and logger in stringify config * Use stringifyConfig * Ignore logs * Add meta to CLI logs * Refactor repo structure * rename repoWorker * renamed worker * Refactor logger location * Refactor main worker * Refactor getRepoConfig * Refactor err * Refactor repo logger * Add config serializer and logger * Refactor redact * Remove stringifyConfig * Refactor onboarding * Set packageFile logger * Refactor package file logic * branch and pr logging * Improve log context * Fix tests part 1 * more test fixes * Fix github init * All tests passing * Rename cli helper * Refactor logger * Add logger tests * Add config serializer tests * Add configParser tests * Fix package file tests * Expand package-file tests * Use defaultConfig * Add package-file tests * Refactor * Finish package-file tests --- .gitignore | 1 + lib/api/github.js | 9 +- lib/api/gitlab.js | 7 +- lib/api/npm.js | 2 +- lib/config/file.js | 2 +- lib/config/index.js | 107 ++------ lib/helpers/changelog.js | 12 +- lib/helpers/cli.js | 36 --- lib/helpers/github-app.js | 2 +- lib/helpers/logger/config-serializer.js | 17 ++ lib/helpers/logger/index.js | 23 ++ lib/helpers/logger/pretty-stdout.js | 74 +++++ lib/helpers/npm.js | 2 +- lib/helpers/package-json.js | 8 +- lib/helpers/versions.js | 2 +- lib/helpers/yarn.js | 2 +- lib/index.js | 187 ------------- lib/logger.js | 19 -- lib/renovate.js | 4 +- lib/workers/branch.js | 54 +++- lib/workers/index.js | 25 ++ lib/{worker.js => workers/package-file.js} | 167 ++---------- lib/workers/pr.js | 10 +- lib/workers/repository.js | 256 ++++++++++++++++++ package.json | 3 +- test/config/__snapshots__/index.spec.js.snap | 16 ++ test/config/index.spec.js | 45 +-- test/helpers/changelog.spec.js | 19 +- .../config-serializer.spec.js.snap | 17 ++ .../__snapshots__/pretty-stdout.spec.js.snap | 6 + test/helpers/logger/config-serializer.spec.js | 20 ++ test/helpers/logger/pretty-stdout.spec.js | 62 +++++ test/helpers/package-json.spec.js | 21 +- .../__snapshots__/package-file.spec.js.snap} | 38 ++- test/workers/__snapshots__/pr.spec.js.snap | 2 +- test/workers/branch.spec.js | 53 ++++ test/{ => workers}/index.spec.js | 2 +- .../package-file.spec.js} | 204 ++++++++------ test/workers/pr.spec.js | 50 ++-- test/workers/repository.spec.js | 26 ++ yarn.lock | 4 + 41 files changed, 976 insertions(+), 640 deletions(-) delete mode 100644 lib/helpers/cli.js create mode 100644 lib/helpers/logger/config-serializer.js create mode 100644 lib/helpers/logger/index.js create mode 100644 lib/helpers/logger/pretty-stdout.js delete mode 100644 lib/index.js delete mode 100644 lib/logger.js create mode 100644 lib/workers/index.js rename lib/{worker.js => workers/package-file.js} (51%) create mode 100644 lib/workers/repository.js create mode 100644 test/config/__snapshots__/index.spec.js.snap create mode 100644 test/helpers/logger/__snapshots__/config-serializer.spec.js.snap create mode 100644 test/helpers/logger/__snapshots__/pretty-stdout.spec.js.snap create mode 100644 test/helpers/logger/config-serializer.spec.js create mode 100644 test/helpers/logger/pretty-stdout.spec.js rename test/{__snapshots__/worker.spec.js.snap => workers/__snapshots__/package-file.spec.js.snap} (64%) rename test/{ => workers}/index.spec.js (76%) rename test/{worker.spec.js => workers/package-file.spec.js} (56%) create mode 100644 test/workers/repository.spec.js diff --git a/.gitignore b/.gitignore index 9ec18cd284..0259fac66b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /coverage /dist .DS_Store +*.log diff --git a/lib/api/github.js b/lib/api/github.js index f9e762ca77..29118debb8 100644 --- a/lib/api/github.js +++ b/lib/api/github.js @@ -1,4 +1,4 @@ -const logger = require('../logger'); +let logger = require('../helpers/logger'); const ghGot = require('gh-got'); const config = {}; @@ -123,8 +123,11 @@ async function getRepos(token, endpoint) { } // Initialize GitHub by getting base branch and SHA -async function initRepo(repoName, token, endpoint) { - logger.debug(`initRepo(${repoName})`); +async function initRepo(repoName, token, endpoint, repoLogger) { + logger.debug(`initRepo(${JSON.stringify(repoName)})`); + if (repoLogger) { + logger = repoLogger; + } if (token) { process.env.GITHUB_TOKEN = token; } else if (!process.env.GITHUB_TOKEN) { diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js index 94c64213ab..5c4ffb71db 100644 --- a/lib/api/gitlab.js +++ b/lib/api/gitlab.js @@ -1,4 +1,4 @@ -const logger = require('../logger'); +let logger = require('../helpers/logger'); const glGot = require('gl-got'); const config = {}; @@ -51,7 +51,10 @@ async function getRepos(token, endpoint) { } // Initialize GitLab by getting base branch -async function initRepo(repoName, token, endpoint) { +async function initRepo(repoName, token, endpoint, repoLogger) { + if (repoLogger) { + logger = repoLogger; + } logger.debug(`initRepo(${repoName})`); if (token) { process.env.GITLAB_TOKEN = token; diff --git a/lib/api/npm.js b/lib/api/npm.js index fd707874cc..e4d476a435 100644 --- a/lib/api/npm.js +++ b/lib/api/npm.js @@ -4,7 +4,7 @@ const got = require('got'); const url = require('url'); const registryUrl = require('registry-url'); const registryAuthToken = require('registry-auth-token'); -const logger = require('../logger'); +const logger = require('../helpers/logger'); module.exports = { setNpmrc, diff --git a/lib/config/file.js b/lib/config/file.js index d844daa099..066a2847ac 100644 --- a/lib/config/file.js +++ b/lib/config/file.js @@ -1,4 +1,4 @@ -const logger = require('../logger'); +const logger = require('../helpers/logger'); const path = require('path'); module.exports = { diff --git a/lib/config/index.js b/lib/config/index.js index 4e182faefb..341091161d 100644 --- a/lib/config/index.js +++ b/lib/config/index.js @@ -1,5 +1,4 @@ -const logger = require('../logger'); -const stringify = require('json-stringify-pretty-compact'); +const logger = require('../helpers/logger'); const githubApi = require('../api/github'); const gitlabApi = require('../api/gitlab'); @@ -10,12 +9,9 @@ const envParser = require('./env'); const githubAppHelper = require('../helpers/github-app'); -let config = null; - module.exports = { parseConfigs, - getCascadedConfig, - getRepositories, + getRepoConfig, }; async function parseConfigs(env, argv) { @@ -27,7 +23,13 @@ async function parseConfigs(env, argv) { const cliConfig = cliParser.getConfig(argv); const envConfig = envParser.getConfig(env); - config = Object.assign({}, defaultConfig, fileConfig, envConfig, cliConfig); + const config = Object.assign( + {}, + defaultConfig, + fileConfig, + envConfig, + cliConfig + ); // Set log level logger.levels('stdout', config.logLevel); @@ -44,13 +46,13 @@ async function parseConfigs(env, argv) { }); } - logger.debug(`Default config = ${redact(defaultConfig)}`); - logger.debug(`File config = ${redact(fileConfig)}`); - logger.debug(`CLI config: ${redact(cliConfig)}`); - logger.debug(`Env config: ${redact(envConfig)}`); + logger.debug({ config: defaultConfig }, 'Default config'); + logger.debug({ config: fileConfig }, 'File config'); + logger.debug({ config: cliConfig }, 'CLI config'); + logger.debug({ config: envConfig }, 'Env config'); // Get global config - logger.debug(`raw config=${redact(config)}`); + logger.debug({ config }, 'Raw config'); // Check platforms and tokens if (config.platform === 'github') { @@ -74,8 +76,7 @@ async function parseConfigs(env, argv) { } config.repositories = await githubAppHelper.getRepositories(config); logger.info(`Found ${config.repositories.length} repositories installed`); - delete config.githubAppKey; - logger.debug(`GitHub App config: ${JSON.stringify(config)}`); + logger.debug({ config }, 'GitHub App config'); } else if (config.autodiscover) { // Autodiscover list of repositories if (config.platform === 'github') { @@ -96,7 +97,7 @@ async function parseConfigs(env, argv) { logger.info( 'The account associated with your token does not have access to any repos' ); - return; + return config; } } else if (!config.repositories || config.repositories.length === 0) { // We need at least one repository defined @@ -105,78 +106,20 @@ async function parseConfigs(env, argv) { ); } - // Configure each repository - config.repositories = config.repositories.map(item => { - // Convert any repository strings to objects - const repo = typeof item === 'string' ? { repository: item } : item; - - // copy across some fields from the base config if not present - repo.token = repo.token || config.token; - repo.platform = repo.platform || config.platform; - repo.onboarding = repo.onboarding || config.onboarding; - repo.endpoint = repo.endpoint || config.endpoint; - - // Set default packageFiles - if (!repo.packageFiles || !repo.packageFiles.length) { - repo.packageFiles = config.packageFiles; - } - - // Expand packageFile format - repo.packageFiles = repo.packageFiles.map(packageFile => { - if (typeof packageFile === 'string') { - return { fileName: packageFile }; - } - return packageFile; - }); - - return repo; - }); - // Print config - logger.debug(`config=${redact(config)}`); + logger.debug({ config }, 'Global config'); // Remove log file entries delete config.logFile; delete config.logFileLevel; + return config; } -function getCascadedConfig(repo, packageFile) { - const cascadedConfig = Object.assign({}, config, repo, packageFile); - // Remove unnecessary fields - delete cascadedConfig.repositories; - delete cascadedConfig.repository; - delete cascadedConfig.fileName; - return cascadedConfig; -} - -function getRepositories() { - return config.repositories; -} - -function redact(inputConfig) { - const redactedConfig = Object.assign({}, inputConfig); - if (redactedConfig.token) { - redactedConfig.token = `${redactedConfig.token.substr(0, 4)}${new Array( - redactedConfig.token.length - 3 - ).join('*')}`; - } - if (redactedConfig.githubAppKey) { - redactedConfig.githubAppKey = '***REDACTED***'; - } - if (inputConfig.repositories) { - redactedConfig.repositories = []; - for (const repository of inputConfig.repositories) { - if (typeof repository !== 'string') { - const redactedRepo = Object.assign({}, repository); - if (redactedRepo.token) { - redactedRepo.token = `${redactedRepo.token.substr(0, 4)}${new Array( - redactedRepo.token.length - 3 - ).join('*')}`; - } - redactedConfig.repositories.push(redactedRepo); - } else { - redactedConfig.repositories.push(repository); - } - } +function getRepoConfig(config, index) { + let repository = config.repositories[index]; + if (typeof repository === 'string') { + repository = { repository }; } - return stringify(redactedConfig); + const returnConfig = Object.assign({}, config, repository); + delete returnConfig.repositories; + return returnConfig; } diff --git a/lib/helpers/changelog.js b/lib/helpers/changelog.js index 03ae98d55c..85b1487771 100644 --- a/lib/helpers/changelog.js +++ b/lib/helpers/changelog.js @@ -1,4 +1,3 @@ -const logger = require('../logger'); const changelog = require('changelog'); module.exports = { @@ -7,7 +6,7 @@ module.exports = { getChangeLog, }; -async function getChangeLogJSON(depName, fromVersion, newVersion) { +async function getChangeLogJSON(depName, fromVersion, newVersion, logger) { logger.debug(`getChangeLogJSON(${depName}, ${fromVersion}, ${newVersion})`); if (!fromVersion || fromVersion === newVersion) { return null; @@ -35,7 +34,12 @@ function getMarkdown(changelogJSON) { } // Get Changelog -async function getChangeLog(depName, fromVersion, newVersion) { - const logJSON = await getChangeLogJSON(depName, fromVersion, newVersion); +async function getChangeLog(depName, fromVersion, newVersion, logger) { + const logJSON = await getChangeLogJSON( + depName, + fromVersion, + newVersion, + logger + ); return getMarkdown(logJSON); } diff --git a/lib/helpers/cli.js b/lib/helpers/cli.js deleted file mode 100644 index 4e66916347..0000000000 --- a/lib/helpers/cli.js +++ /dev/null @@ -1,36 +0,0 @@ -// Code derived from https://github.com/hadfieldn/node-bunyan-RenovateStream and heavily edited -// Neither fork nor original repo appear to be maintained - -const Stream = require('stream').Stream; -const util = require('util'); -const chalk = require('chalk'); - -const levels = { - 10: chalk.gray('TRACE'), - 20: chalk.blue('DEBUG'), - 30: chalk.green(' INFO'), - 40: chalk.magenta(' WARN'), - 50: chalk.red('ERROR'), - 60: chalk.bgRed('FATAL'), -}; - -function RenovateStream() { - this.readable = true; - this.writable = true; - Stream.call(this); - - this.formatRecord = function formatRecord(rec) { - const level = levels[rec.level]; - const msg = `${rec.msg.split(/\r?\n/).join('\n ')}`; - return util.format('%s: %s\n', level, msg); - }; -} - -util.inherits(RenovateStream, Stream); - -RenovateStream.prototype.write = function write(data) { - this.emit('data', this.formatRecord(data)); - return true; -}; - -module.exports = RenovateStream; diff --git a/lib/helpers/github-app.js b/lib/helpers/github-app.js index 29d40f9fa2..8e115e54a0 100644 --- a/lib/helpers/github-app.js +++ b/lib/helpers/github-app.js @@ -1,5 +1,5 @@ const jwt = require('jsonwebtoken'); -const logger = require('../logger'); +const logger = require('../helpers/logger'); const ghApi = require('../api/github'); module.exports = { diff --git a/lib/helpers/logger/config-serializer.js b/lib/helpers/logger/config-serializer.js new file mode 100644 index 0000000000..afc3c76d67 --- /dev/null +++ b/lib/helpers/logger/config-serializer.js @@ -0,0 +1,17 @@ +const traverse = require('traverse'); + +module.exports = configSerializer; + +function configSerializer(config) { + const redactedFields = ['token', 'githubAppKey']; + const functionFields = ['api', 'logger']; + // eslint-disable-next-line array-callback-return + return traverse(config).map(function scrub(val) { + if (val && redactedFields.indexOf(this.key) !== -1) { + this.update('***********'); + } + if (val && functionFields.indexOf(this.key) !== -1) { + this.update('[Function]'); + } + }); +} diff --git a/lib/helpers/logger/index.js b/lib/helpers/logger/index.js new file mode 100644 index 0000000000..5a19124725 --- /dev/null +++ b/lib/helpers/logger/index.js @@ -0,0 +1,23 @@ +const bunyan = require('bunyan'); +const PrettyStdout = require('./pretty-stdout').RenovateStream; +const configSerializer = require('./config-serializer'); + +const prettyStdOut = new PrettyStdout(); +prettyStdOut.pipe(process.stdout); + +const logger = bunyan.createLogger({ + name: 'renovate', + serializers: { + config: configSerializer, + }, + streams: [ + { + name: 'stdout', + level: process.env.LOG_LEVEL || 'info', + type: 'raw', + stream: prettyStdOut, + }, + ], +}); + +module.exports = logger; diff --git a/lib/helpers/logger/pretty-stdout.js b/lib/helpers/logger/pretty-stdout.js new file mode 100644 index 0000000000..9679d1bb2d --- /dev/null +++ b/lib/helpers/logger/pretty-stdout.js @@ -0,0 +1,74 @@ +// Code derived from https://github.com/hadfieldn/node-bunyan-RenovateStream and heavily edited +// Neither fork nor original repo appear to be maintained + +const Stream = require('stream').Stream; +const util = require('util'); +const chalk = require('chalk'); +const stringify = require('json-stringify-pretty-compact'); + +const levels = { + 10: chalk.gray('TRACE'), + 20: chalk.blue('DEBUG'), + 30: chalk.green(' INFO'), + 40: chalk.magenta(' WARN'), + 50: chalk.red('ERROR'), + 60: chalk.bgRed('FATAL'), +}; + +function indent(str, leading = false) { + const prefix = leading ? ' ' : ''; + return prefix + str.split(/\r?\n/).join('\n '); +} + +function getMeta(rec) { + if (!rec) { + return ''; + } + const metaFields = [ + 'repository', + 'packageFile', + 'dependency', + 'branch', + ].filter(elem => rec[elem]); + if (!metaFields.length) { + return ''; + } + const metaStr = metaFields.map(field => `${field}=${rec[field]}`).join(', '); + return chalk.gray(` (${metaStr})`); +} + +function getDetails(rec) { + if (!rec || !rec.config) { + return ''; + } + return `${indent(stringify(rec.config), true)}\n`; +} + +function formatRecord(rec) { + const level = levels[rec.level]; + const msg = `${indent(rec.msg)}`; + const meta = getMeta(rec); + const details = getDetails(rec); + return util.format('%s: %s%s\n%s', level, msg, meta, details); +} + +function RenovateStream() { + this.readable = true; + this.writable = true; + Stream.call(this); +} + +util.inherits(RenovateStream, Stream); + +RenovateStream.prototype.write = function write(data) { + this.emit('data', formatRecord(data)); + return true; +}; + +module.exports = { + indent, + getMeta, + getDetails, + formatRecord, + RenovateStream, +}; diff --git a/lib/helpers/npm.js b/lib/helpers/npm.js index 162c357fee..3ef62334bf 100644 --- a/lib/helpers/npm.js +++ b/lib/helpers/npm.js @@ -1,4 +1,4 @@ -const logger = require('../logger'); +const logger = require('../helpers/logger'); const fs = require('fs'); const cp = require('child_process'); const tmp = require('tmp'); diff --git a/lib/helpers/package-json.js b/lib/helpers/package-json.js index d71bc90bac..a4ad33dc5e 100644 --- a/lib/helpers/package-json.js +++ b/lib/helpers/package-json.js @@ -1,4 +1,3 @@ -const logger = require('../logger'); const _ = require('lodash'); module.exports = { @@ -24,7 +23,7 @@ function extractDependencies(packageJson, sections) { }, []); } -function setNewValue(currentFileContent, depType, depName, newVersion) { +function setNewValue(currentFileContent, depType, depName, newVersion, logger) { logger.debug(`setNewValue: ${depType}.${depName} = ${newVersion}`); const parsedContents = JSON.parse(currentFileContent); // Save the old version @@ -52,7 +51,8 @@ function setNewValue(currentFileContent, depType, depName, newVersion) { currentFileContent, searchIndex, searchString, - newString + newString, + logger ); // Compare the parsed JSON structure of old and new if (_.isEqual(parsedContents, JSON.parse(testContent))) { @@ -74,7 +74,7 @@ function matchAt(content, index, match) { } // Replace oldString with newString at location index of content -function replaceAt(content, index, oldString, newString) { +function replaceAt(content, index, oldString, newString, logger) { logger.debug(`Replacing ${oldString} with ${newString} at index ${index}`); return ( content.substr(0, index) + diff --git a/lib/helpers/versions.js b/lib/helpers/versions.js index c0ae33244a..357ed95123 100644 --- a/lib/helpers/versions.js +++ b/lib/helpers/versions.js @@ -1,4 +1,4 @@ -const logger = require('../logger'); +const logger = require('../helpers/logger'); const semver = require('semver'); const stable = require('semver-stable'); const _ = require('lodash'); diff --git a/lib/helpers/yarn.js b/lib/helpers/yarn.js index 8b02ad2326..2a52926cb3 100644 --- a/lib/helpers/yarn.js +++ b/lib/helpers/yarn.js @@ -1,4 +1,4 @@ -const logger = require('../logger'); +const logger = require('../helpers/logger'); const fs = require('fs'); const cp = require('child_process'); const tmp = require('tmp'); diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 6822a0d8af..0000000000 --- a/lib/index.js +++ /dev/null @@ -1,187 +0,0 @@ -const stringify = require('json-stringify-pretty-compact'); -const logger = require('./logger'); -const configParser = require('./config'); -const githubApi = require('./api/github'); -const gitlabApi = require('./api/gitlab'); -const npmApi = require('./api/npm'); -const defaultsParser = require('./config/defaults'); -const ini = require('ini'); - -// Require main source -const worker = require('./worker'); - -module.exports = { - start, - processRepo, - setNpmrc, -}; - -// This will be github or others -let api; - -async function start() { - // Parse config - try { - await configParser.parseConfigs(process.env, process.argv); - // Iterate through repositories sequentially - for (const repo of configParser.getRepositories()) { - await processRepo(repo); - } - logger.info('Renovate finished'); - } catch (error) { - logger.error(error.message); - } -} - -// Queue package files in sequence within a repo -async function processRepo(repo) { - logger.info(`Processing repository ${repo.repository}`); - // Take a copy of the config, as we will modify it - const config = Object.assign({}, repo); - if (config.platform === 'github') { - api = githubApi; - } else if (config.platform === 'gitlab') { - api = gitlabApi; - } else { - logger.error( - `Unknown platform ${config.platform} for repository ${repo.repository}` - ); - return; - } - logger.debug(`Repository config:\n${stringify(config)}`); - try { - // Initialize repo - await api.initRepo(config.repository, config.token, config.endpoint); - await mergeRenovateJson(config); - const isConfigured = await checkIfConfigured(config); - if (isConfigured === false) { - return; - } - await setNpmrc(); - await findPackageFiles(config); - const upgrades = await getAllRepoUpgrades(config); - await worker.processUpgrades(upgrades); - } catch (error) { - throw error; - } -} - -// Check for config in `renovate.json` -async function setNpmrc() { - try { - let npmrc = null; - const npmrcContent = await api.getFileContent('.npmrc'); - if (npmrcContent) { - logger.debug('Found .npmrc file in repository'); - npmrc = ini.parse(npmrcContent); - } - npmApi.setNpmrc(npmrc); - } catch (err) { - logger.error('Failed to set .npmrc'); - } -} - -// Check for config in `renovate.json` -async function mergeRenovateJson(config) { - const renovateJson = await api.getFileJson('renovate.json'); - if (renovateJson) { - logger.debug(`renovate.json config: ${stringify(renovateJson)}`); - Object.assign(config, renovateJson, { repoConfigured: true }); - } else { - logger.debug('No renovate.json found'); - } -} - -async function checkIfConfigured(config) { - logger.debug('Checking if repo is configured'); - // Check if repository is configured - if (config.repoConfigured || config.onboarding === false) { - logger.debug('Repo is configured or onboarding disabled'); - return true; - } - const pr = await api.findPr('renovate/configure', 'Configure Renovate'); - if (pr) { - if (pr.isClosed) { - logger.debug('Closed Configure Renovate PR found - continuing'); - return true; - } - // PR exists but hasn't been closed yet - logger.error(`Close PR #${pr.displayNumber} before continuing`); - return false; - } - await configureRepository(config); - return false; -} - -async function configureRepository(config) { - const defaultConfig = defaultsParser.getConfig(); - delete defaultConfig.onboarding; - delete defaultConfig.platform; - delete defaultConfig.endpoint; - delete defaultConfig.token; - delete defaultConfig.autodiscover; - delete defaultConfig.githubAppId; - delete defaultConfig.githubAppKey; - delete defaultConfig.repositories; - delete defaultConfig.logLevel; - let prBody = `Welcome to [Renovate](https://keylocation.sg/our-tech/renovate)! Once you close this Pull Request, we will begin keeping your dependencies up-to-date via automated Pull Requests. - -The [Configuration](https://github.com/singapore/renovate/blob/master/docs/configuration.md) and [Configuration FAQ](https://github.com/singapore/renovate/blob/master/docs/faq.md) documents should be helpful. - -#### Important! - -You do not need to *merge* this Pull Request - renovate will begin even if it's closed *unmerged*. -In fact, you only need to add a \`renovate.json\` file to your repository if you wish to override any default settings. The file is included as part of this PR only in case you wish to change default settings before you start. -If the default settings are all suitable for you, simply close this Pull Request unmerged and your first renovation will begin the next time the program is run.`; - - if (config.platform === 'gitlab') { - defaultConfig.platform = 'gitlab'; - prBody = prBody.replace(/Pull Request/g, 'Merge Request'); - } - const defaultConfigString = `${stringify(defaultConfig)}\n`; - await api.commitFilesToBranch( - 'renovate/configure', - [ - { - name: 'renovate.json', - contents: defaultConfigString, - }, - ], - 'Add renovate.json' - ); - const pr = await api.createPr( - 'renovate/configure', - 'Configure Renovate', - prBody - ); - logger.info(`Created ${pr.displayNumber} for configuration`); -} - -// Ensure config contains packageFiles -async function findPackageFiles(config) { - if (config.packageFiles.length === 0) { - // autodiscover filenames if none manually configured - const fileNames = await api.findFilePaths('package.json'); - // Map to config structure - const packageFiles = fileNames.map(fileName => ({ fileName })); - Object.assign(config, { packageFiles }); - } -} - -async function getAllRepoUpgrades(repo) { - let upgrades = []; - for (let packageFile of repo.packageFiles) { - if (typeof packageFile === 'string') { - packageFile = { fileName: packageFile }; - } - const cascadedConfig = configParser.getCascadedConfig(repo, packageFile); - upgrades = upgrades.concat( - await worker.processPackageFile( - repo.repository, - packageFile.fileName, - cascadedConfig - ) - ); - } - return upgrades; -} diff --git a/lib/logger.js b/lib/logger.js deleted file mode 100644 index fd7ceabc75..0000000000 --- a/lib/logger.js +++ /dev/null @@ -1,19 +0,0 @@ -const bunyan = require('bunyan'); -const CliHelper = require('./helpers/cli'); - -const cliHelper = new CliHelper(); -cliHelper.pipe(process.stdout); - -const logger = bunyan.createLogger({ - name: 'myapp', - streams: [ - { - name: 'stdout', - level: process.env.LOG_LEVEL || 'info', - type: 'raw', - stream: cliHelper, - }, - ], -}); - -module.exports = logger; diff --git a/lib/renovate.js b/lib/renovate.js index 398a59b7dd..6fb57b2856 100644 --- a/lib/renovate.js +++ b/lib/renovate.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -const renovate = require('./index'); +const renovateWorker = require('./workers/index'); -renovate.start(); +renovateWorker.start(); diff --git a/lib/workers/branch.js b/lib/workers/branch.js index c03aec59dd..9a91e0e3b6 100644 --- a/lib/workers/branch.js +++ b/lib/workers/branch.js @@ -1,12 +1,14 @@ -const logger = require('../logger'); const handlebars = require('handlebars'); const packageJsonHelper = require('../helpers/package-json'); const npmHelper = require('../helpers/npm'); const yarnHelper = require('../helpers/yarn'); +const prWorker = require('./pr'); +let logger = require('../helpers/logger'); module.exports = { getParentBranch, ensureBranch, + updateBranch, }; async function getParentBranch(branchName, config) { @@ -53,7 +55,7 @@ async function getParentBranch(branchName, config) { // Ensure branch exists with appropriate content async function ensureBranch(upgrades) { - logger.debug(`ensureBranch(${JSON.stringify(upgrades)})`); + logger.debug({ config: upgrades }, 'ensureBranch'); // Use the first upgrade for all the templates const branchName = handlebars.compile(upgrades[0].branchName)(upgrades[0]); // parentBranch is the branch we will base off @@ -93,7 +95,8 @@ async function ensureBranch(upgrades) { packageFiles[upgrade.packageFile], upgrade.depType, upgrade.depName, - upgrade.newVersion + upgrade.newVersion, + logger ); if (packageFiles[upgrade.packageFile] === newContent) { logger.debug('packageFile content unchanged'); @@ -188,3 +191,48 @@ async function ensureBranch(upgrades) { // Return true as branch exists return true; } + +async function updateBranch(upgrades, parentLogger) { + const upgrade0 = upgrades[0]; + // Use templates to generate strings + const branchName = handlebars.compile(upgrade0.branchName)(upgrade0); + const prTitle = handlebars.compile(upgrade0.prTitle)(upgrade0); + + logger = parentLogger.child({ + repository: upgrade0.repository, + branch: branchName, + }); + + const upgradeCount = upgrades.length === 1 + ? '1 upgrade' + : `${upgrades.length} upgrades`; + logger.info( + `Branch '${branchName}' has ${upgradeCount}: ${upgrades.map( + upgrade => upgrade.depName + )}` + ); + + try { + if ( + upgrade0.upgradeType !== 'maintainYarnLock' && + upgrade0.groupName === null && + !upgrade0.recreateClosed && + (await upgrade0.api.checkForClosedPr(branchName, prTitle)) + ) { + logger.info( + `Skipping ${branchName} upgrade as matching closed PR already existed` + ); + return; + } + const branchCreated = await module.exports.ensureBranch(upgrades); + if (branchCreated) { + const pr = await prWorker.ensurePr(upgrades, logger); + if (pr) { + await prWorker.checkAutoMerge(pr, upgrade0, logger); + } + } + } catch (error) { + logger.error(`Error updating branch ${branchName}: ${error}`); + // Don't throw here - we don't want to stop the other renovations + } +} diff --git a/lib/workers/index.js b/lib/workers/index.js new file mode 100644 index 0000000000..6c3a3e8b27 --- /dev/null +++ b/lib/workers/index.js @@ -0,0 +1,25 @@ +const logger = require('../helpers/logger'); +const configParser = require('../config'); +const repositoryWorker = require('./repository'); + +module.exports = { + start, +}; + +async function start() { + // Parse config + try { + logger.info('Renovate starting'); + const config = await configParser.parseConfigs(process.env, process.argv); + // Iterate through repositories sequentially + for (let index = 0; index < config.repositories.length; index += 1) { + const repoConfig = configParser.getRepoConfig(config, index); + repoConfig.logger = logger; + await repositoryWorker.processRepo(repoConfig); + } + logger.info('Renovate finished'); + } catch (err) { + logger.fatal(`Renovate fatal error: ${err.message}`); + logger.error(err); + } +} diff --git a/lib/worker.js b/lib/workers/package-file.js similarity index 51% rename from lib/worker.js rename to lib/workers/package-file.js index fa47ff8fe7..712ecb028d 100644 --- a/lib/worker.js +++ b/lib/workers/package-file.js @@ -1,33 +1,29 @@ -const logger = require('./logger'); -const stringify = require('json-stringify-pretty-compact'); -const handlebars = require('handlebars'); -const versionsHelper = require('./helpers/versions'); -const packageJson = require('./helpers/package-json'); -const npmApi = require('./api/npm'); -const prWorker = require('./workers/pr'); -const branchWorker = require('./workers/branch'); +// API +const npmApi = require('../api/npm'); +// Helpers +const packageJson = require('../helpers/package-json'); +const versionsHelper = require('../helpers/versions'); -let config; +let logger = require('../helpers/logger'); module.exports = { processPackageFile, - findUpgrades, - processUpgrades, - updateBranch, - removeStandaloneBranches, assignDepConfigs, + findUpgrades, getDepTypeConfig, }; // This function manages the queue per-package file -async function processPackageFile(repoName, packageFile, packageConfig) { - // Initialize globals - config = Object.assign({}, packageConfig); - config.packageFile = packageFile; +async function processPackageFile(config) { + // Initialize logger + logger = config.logger.child({ + repository: config.repository, + packageFile: config.packageFile, + }); - logger.info(`Processing package file ${repoName}:${packageFile}`); + logger.info(`Processing package file`); - const packageContent = await config.api.getFileJson(packageFile); + const packageContent = await config.api.getFileJson(config.packageFile); if (!packageContent) { logger.warn('No package.json content found - skipping'); @@ -37,7 +33,8 @@ async function processPackageFile(repoName, packageFile, packageConfig) { // Check for renovate config inside the package.json if (packageContent.renovate) { logger.debug( - `package.json>renovate config:\n${stringify(packageContent.renovate)}` + { config: packageContent.renovate }, + 'package.json>renovate config' ); Object.assign(config, packageContent.renovate, { repoConfigured: true }); } @@ -63,9 +60,9 @@ async function processPackageFile(repoName, packageFile, packageConfig) { dependencies = dependencies.filter( dependency => config.ignoreDeps.indexOf(dependency.depName) === -1 ); - dependencies = assignDepConfigs(config, dependencies); + dependencies = module.exports.assignDepConfigs(config, dependencies); // Find all upgrades for remaining dependencies - const upgrades = await findUpgrades(dependencies); + const upgrades = await module.exports.findUpgrades(dependencies); // Process all upgrades sequentially if (config.maintainYarnLock) { const upgrade = Object.assign({}, config, { @@ -109,6 +106,7 @@ function assignDepConfigs(inputConfig, deps) { } }); } + // TODO: clean this up delete returnDep.config.depType; delete returnDep.config.depTypes; delete returnDep.config.enabled; @@ -132,18 +130,6 @@ function assignDepConfigs(inputConfig, deps) { }); } -function getDepTypeConfig(depTypes, depTypeName) { - let depTypeConfig = {}; - if (depTypes) { - depTypes.forEach(depType => { - if (typeof depType !== 'string' && depType.depType === depTypeName) { - depTypeConfig = depType; - } - }); - } - return depTypeConfig; -} - async function findUpgrades(dependencies) { const allUpgrades = []; // findDepUpgrades can add more than one upgrade to allUpgrades @@ -181,111 +167,14 @@ async function findUpgrades(dependencies) { return allUpgrades; } -async function processUpgrades(upgrades) { - if (upgrades.length) { - const upgradeCount = upgrades.length === 1 - ? '1 dependency upgrade' - : `${upgrades.length} dependency upgrades`; - logger.info(`Processing ${upgradeCount}`); - } else { - logger.info('No upgrades to process'); - } - logger.debug(`All upgrades: ${JSON.stringify(upgrades)}`); - const branchUpgrades = {}; - for (const upgrade of upgrades) { - const flattened = Object.assign({}, upgrade.config, upgrade); - delete flattened.config; - if (flattened.upgradeType === 'pin') { - flattened.isPin = true; - } else if (flattened.upgradeType === 'major') { - flattened.isMajor = true; - } else if (flattened.upgradeType === 'minor') { - flattened.isMinor = true; - } - // Check whether to use a group name - let branchName; - if (flattened.groupName) { - logger.debug( - `Dependency ${flattened.depName} is part of group '${flattened.groupName}'` - ); - flattened.groupSlug = - flattened.groupSlug || - flattened.groupName.toLowerCase().replace(/[^a-z0-9+]+/g, '-'); - branchName = handlebars.compile(flattened.groupBranchName)(flattened); - logger.debug(`branchName=${branchName}`); - if (branchUpgrades[branchName]) { - // flattened.branchName = flattened.groupBranchName; - flattened.commitMessage = flattened.groupCommitMessage; - flattened.prTitle = flattened.groupPrTitle; - flattened.prBody = flattened.groupPrBody; - } - } else { - branchName = handlebars.compile(flattened.branchName)(flattened); - } - branchUpgrades[branchName] = branchUpgrades[branchName] || []; - branchUpgrades[branchName] = [flattened].concat(branchUpgrades[branchName]); - } - logger.debug(`Branched upgrades: ${JSON.stringify(branchUpgrades)}`); - for (const branch of Object.keys(branchUpgrades)) { - await module.exports.removeStandaloneBranches(branchUpgrades[branch]); - await module.exports.updateBranch(branchUpgrades[branch]); - } -} - -async function removeStandaloneBranches(upgrades) { - if (upgrades.length > 1) { - for (const upgrade of upgrades) { - const standaloneBranchName = handlebars.compile(upgrade.branchName)( - upgrade - ); - logger.debug(`Need to delete branch ${standaloneBranchName}`); - try { - await upgrade.api.deleteBranch(standaloneBranchName); - } catch (err) { - logger.debug(`Couldn't delete branch ${standaloneBranchName}`); - } - // Rename to group branchName - upgrade.branchName = upgrade.groupBranchName; - } - } -} - -async function updateBranch(upgrades) { - // Use templates to generate strings - const upgrade0 = upgrades[0]; - const branchName = handlebars.compile(upgrade0.branchName)(upgrade0); - const prTitle = handlebars.compile(upgrade0.prTitle)(upgrade0); - - const upgradeCount = upgrades.length === 1 - ? '1 upgrade' - : `${upgrades.length} upgrades`; - logger.info( - `Branch '${branchName}' has ${upgradeCount}: ${upgrades.map( - upgrade => upgrade.depName - )}` - ); - - try { - if ( - upgrade0.upgradeType !== 'maintainYarnLock' && - upgrade0.groupName === null && - !upgrade0.recreateClosed && - (await upgrade0.api.checkForClosedPr(branchName, prTitle)) - ) { - logger.info( - `Skipping ${branchName} upgrade as matching closed PR already existed` - ); - return; - } - const branchCreated = await branchWorker.ensureBranch(upgrades); - if (branchCreated) { - const pr = await prWorker.ensurePr(upgrades); - if (pr) { - await prWorker.checkAutoMerge(pr, upgrade0); +function getDepTypeConfig(depTypes, depTypeName) { + let depTypeConfig = {}; + if (depTypes) { + depTypes.forEach(depType => { + if (typeof depType !== 'string' && depType.depType === depTypeName) { + depTypeConfig = depType; } - } - } catch (error) { - logger.error(`Error updating branch ${branchName}: ${error}`); - // Don't throw here - we don't want to stop the other renovations + }); } + return depTypeConfig; } diff --git a/lib/workers/pr.js b/lib/workers/pr.js index 003c2d80cd..8c1884f804 100644 --- a/lib/workers/pr.js +++ b/lib/workers/pr.js @@ -1,4 +1,3 @@ -const logger = require('../logger'); const handlebars = require('handlebars'); const changelogHelper = require('../helpers/changelog'); @@ -8,8 +7,8 @@ module.exports = { }; // Ensures that PR exists with matching title/body -async function ensurePr(upgrades) { - logger.debug(`ensurePr(${JSON.stringify(upgrades)})`); +async function ensurePr(upgrades, logger) { + logger.debug({ config: upgrades }, 'ensurePr'); // If there is a group, it will use the config of the first upgrade in the array const config = Object.assign({}, upgrades[0]); config.upgrades = []; @@ -57,7 +56,8 @@ async function ensurePr(upgrades) { const logJSON = await changelogHelper.getChangeLogJSON( upgrade.depName, upgrade.changeLogFromVersion, - upgrade.changeLogToVersion + upgrade.changeLogToVersion, + logger ); // Store changelog markdown for backwards compatibility if (logJSON) { @@ -140,7 +140,7 @@ async function ensurePr(upgrades) { return null; } -async function checkAutoMerge(pr, config) { +async function checkAutoMerge(pr, config, logger) { logger.debug(`Checking #${pr.number} for automerge`); if (config.automergeEnabled && config.automergeType === 'pr') { logger.info('PR is configured for automerge'); diff --git a/lib/workers/repository.js b/lib/workers/repository.js new file mode 100644 index 0000000000..7329ee5cfb --- /dev/null +++ b/lib/workers/repository.js @@ -0,0 +1,256 @@ +// Global requires +const handlebars = require('handlebars'); +const ini = require('ini'); +let logger = require('../helpers/logger'); +const stringify = require('json-stringify-pretty-compact'); +// API +const githubApi = require('../api/github'); +const gitlabApi = require('../api/gitlab'); +const npmApi = require('../api/npm'); +// Config +const defaultsParser = require('../config/defaults'); +// Workers +const packageFileWorker = require('./package-file'); +const branchWorker = require('./branch'); + +module.exports = { + processRepo, + processUpgrades, + removeStandaloneBranches, +}; + +// This will be github or others +let api; + +// Queue package files in sequence within a repo +async function processRepo(config) { + logger = config.logger.child({ repository: config.repository }); + config.logger = logger; // eslint-disable-line no-param-reassign + logger.info('Renovating repository'); + logger.debug({ config }, 'processRepo'); + if (config.platform === 'github') { + api = githubApi; + } else if (config.platform === 'gitlab') { + api = gitlabApi; + } else { + // TODO: throw this? + logger.error( + `Unknown platform ${config.platform} for repository ${config.repository}` + ); + return; + } + try { + // Initialize repo + await api.initRepo( + config.repository, + config.token, + config.endpoint, + logger + ); + // Override settings with renovate.json if present + await mergeRenovateJson(config); + // Check that the repository is onboarded + const isOnboarded = await checkIfOnboarded(config); + if (isOnboarded === false) { + return; + } + // Check for presence of .npmrc in repository + await setNpmrc(config); + // Detect package files if none already configured + await detectPackageFiles(config); + const upgrades = await getAllRepoUpgrades(config); + await module.exports.processUpgrades(upgrades); + } catch (error) { + throw error; + } + logger.info('Finished repository'); +} + +// Check for config in `renovate.json` +async function mergeRenovateJson(config) { + const renovateJson = await api.getFileJson('renovate.json'); + if (renovateJson) { + logger.debug({ config: renovateJson }, 'renovate.json config'); + Object.assign(config, renovateJson, { repoConfigured: true }); + } else { + logger.debug('No renovate.json found'); + } +} + +// Check for .npmrc in repository and pass it to npm api if found +async function setNpmrc() { + try { + let npmrc = null; + const npmrcContent = await api.getFileContent('.npmrc'); + if (npmrcContent) { + logger.debug('Found .npmrc file in repository'); + npmrc = ini.parse(npmrcContent); + } + npmApi.setNpmrc(npmrc); + } catch (err) { + logger.error('Failed to set .npmrc'); + } +} + +async function checkIfOnboarded(config) { + logger.debug('Checking if repo is configured'); + // Check if repository is configured + if (config.repoConfigured || config.onboarding === false) { + logger.debug('Repo is configured or onboarding disabled'); + return true; + } + const pr = await api.findPr('renovate/configure', 'Configure Renovate'); + if (pr) { + if (pr.isClosed) { + logger.debug('Closed Configure Renovate PR found - continuing'); + return true; + } + // PR exists but hasn't been closed yet + logger.error(`Close PR #${pr.displayNumber} before continuing`); + return false; + } + await onboardRepository(config); + return false; +} + +// Ensure config contains packageFiles +async function detectPackageFiles(config) { + if (config.packageFiles.length === 0) { + // autodiscover filenames if none manually configured + const fileNames = await api.findFilePaths('package.json'); + // Map to config structure + const packageFiles = fileNames.map(fileName => ({ fileName })); + Object.assign(config, { packageFiles }); + } +} + +async function onboardRepository(config) { + const defaultConfig = defaultsParser.getConfig(); + delete defaultConfig.onboarding; + delete defaultConfig.platform; + delete defaultConfig.endpoint; + delete defaultConfig.token; + delete defaultConfig.autodiscover; + delete defaultConfig.githubAppId; + delete defaultConfig.githubAppKey; + delete defaultConfig.repositories; + delete defaultConfig.logLevel; + let prBody = `Welcome to [Renovate](https://keylocation.sg/our-tech/renovate)! Once you close this Pull Request, we will begin keeping your dependencies up-to-date via automated Pull Requests. + +The [Configuration](https://github.com/singapore/renovate/blob/master/docs/configuration.md) and [Configuration FAQ](https://github.com/singapore/renovate/blob/master/docs/faq.md) documents should be helpful. + +#### Important! + +You do not need to *merge* this Pull Request - renovate will begin even if it's closed *unmerged*. +In fact, you only need to add a \`renovate.json\` file to your repository if you wish to override any default settings. The file is included as part of this PR only in case you wish to change default settings before you start. +If the default settings are all suitable for you, simply close this Pull Request unmerged and your first renovation will begin the next time the program is run.`; + + if (config.platform === 'gitlab') { + defaultConfig.platform = 'gitlab'; + prBody = prBody.replace(/Pull Request/g, 'Merge Request'); + } + const defaultConfigString = `${stringify(defaultConfig)}\n`; + await api.commitFilesToBranch( + 'renovate/configure', + [ + { + name: 'renovate.json', + contents: defaultConfigString, + }, + ], + 'Add renovate.json' + ); + const pr = await api.createPr( + 'renovate/configure', + 'Configure Renovate', + prBody + ); + logger.info(`Created ${pr.displayNumber} for configuration`); +} + +async function getAllRepoUpgrades(config) { + logger.info('getAllRepoUpgrades'); + let upgrades = []; + for (let packageFile of config.packageFiles) { + if (typeof packageFile === 'string') { + packageFile = { fileName: packageFile }; + } + const cascadedConfig = Object.assign({}, config, packageFile); + // Remove unnecessary fields + cascadedConfig.packageFile = cascadedConfig.fileName; + delete cascadedConfig.fileName; + upgrades = upgrades.concat( + await packageFileWorker.processPackageFile(cascadedConfig) + ); + } + return upgrades; +} + +async function processUpgrades(upgrades) { + if (upgrades.length) { + const upgradeCount = upgrades.length === 1 + ? '1 dependency upgrade' + : `${upgrades.length} dependency upgrades`; + logger.info(`Processing ${upgradeCount}`); + } else { + logger.info('No upgrades to process'); + } + logger.debug({ config: upgrades }, 'All upgrades'); + const branchUpgrades = {}; + for (const upgrade of upgrades) { + const flattened = Object.assign({}, upgrade.config, upgrade); + delete flattened.config; + if (flattened.upgradeType === 'pin') { + flattened.isPin = true; + } else if (flattened.upgradeType === 'major') { + flattened.isMajor = true; + } else if (flattened.upgradeType === 'minor') { + flattened.isMinor = true; + } + // Check whether to use a group name + let branchName; + if (flattened.groupName) { + logger.debug( + `Dependency ${flattened.depName} is part of group '${flattened.groupName}'` + ); + flattened.groupSlug = + flattened.groupSlug || + flattened.groupName.toLowerCase().replace(/[^a-z0-9+]+/g, '-'); + branchName = handlebars.compile(flattened.groupBranchName)(flattened); + logger.debug(`branchName=${branchName}`); + if (branchUpgrades[branchName]) { + // flattened.branchName = flattened.groupBranchName; + flattened.commitMessage = flattened.groupCommitMessage; + flattened.prTitle = flattened.groupPrTitle; + flattened.prBody = flattened.groupPrBody; + } + } else { + branchName = handlebars.compile(flattened.branchName)(flattened); + } + branchUpgrades[branchName] = branchUpgrades[branchName] || []; + branchUpgrades[branchName] = [flattened].concat(branchUpgrades[branchName]); + } + logger.debug({ config: branchUpgrades }, 'Branched upgrades'); + for (const branch of Object.keys(branchUpgrades)) { + await module.exports.removeStandaloneBranches(branchUpgrades[branch]); + await branchWorker.updateBranch(branchUpgrades[branch], logger); + } +} + +async function removeStandaloneBranches(upgrades) { + if (upgrades.length > 1) { + for (const upgrade of upgrades) { + const standaloneBranchName = handlebars.compile(upgrade.branchName)( + upgrade + ); + logger.debug(`Need to delete branch ${standaloneBranchName}`); + try { + await upgrade.api.deleteBranch(standaloneBranchName); + } catch (err) { + logger.debug(`Couldn't delete branch ${standaloneBranchName}`); + } + // Rename to group branchName + upgrade.branchName = upgrade.groupBranchName; + } + } +} diff --git a/package.json b/package.json index 8e5a1b5bfa..03fe526dff 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "semver": "5.3.0", "semver-stable": "2.0.4", "semver-utils": "1.1.1", - "tmp": "0.0.31" + "tmp": "0.0.31", + "traverse": "0.6.6" }, "devDependencies": { "babel-cli": "6.24.1", diff --git a/test/config/__snapshots__/index.spec.js.snap b/test/config/__snapshots__/index.spec.js.snap new file mode 100644 index 0000000000..2f519246fb --- /dev/null +++ b/test/config/__snapshots__/index.spec.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`config/index .getRepoConfig(config, index) handles object repos 1`] = ` +Object { + "global": "b", + "repoField": "g", + "repository": "e/f", +} +`; + +exports[`config/index .getRepoConfig(config, index) massages string repos 1`] = ` +Object { + "global": "b", + "repository": "c/d", +} +`; diff --git a/test/config/index.spec.js b/test/config/index.spec.js index 585ba8ae40..b96bd87116 100644 --- a/test/config/index.spec.js +++ b/test/config/index.spec.js @@ -1,5 +1,4 @@ const argv = require('../_fixtures/config/argv'); -const should = require('chai').should(); describe('config/index', () => { describe('.parseConfigs(env, defaultArgv)', () => { @@ -150,23 +149,37 @@ describe('config/index', () => { expect(ghGot.mock.calls.length).toBe(1); expect(glGot.mock.calls.length).toBe(0); }); - it('supports repositories in CLI', async () => { - const env = {}; - defaultArgv = defaultArgv.concat(['--token=abc', 'foo']); + it('adds a log file', async () => { + const env = { GITHUB_TOKEN: 'abc', RENOVATE_LOG_FILE: 'debug.log' }; + defaultArgv = defaultArgv.concat(['--autodiscover']); + ghGot.mockImplementationOnce(() => ({ + body: [], + })); await configParser.parseConfigs(env, defaultArgv); - const repos = configParser.getRepositories(); - should.exist(repos); - repos.should.have.length(1); - repos[0].repository.should.eql('foo'); + expect(ghGot.mock.calls.length).toBe(1); + expect(glGot.mock.calls.length).toBe(0); }); - it('gets cascaded config', async () => { - const env = { RENOVATE_CONFIG_FILE: 'test/_fixtures/config/file.js' }; - await configParser.parseConfigs(env, defaultArgv); - const repo = configParser.getRepositories().pop(); - should.exist(repo); - const cascadedConfig = configParser.getCascadedConfig(repo, null); - should.exist(cascadedConfig.token); - should.exist(cascadedConfig.recreateClosed); + }); + describe('.getRepoConfig(config, index)', () => { + let configParser; + beforeEach(() => { + configParser = require('../../lib/config/index.js'); + }); + const config = { + global: 'b', + repositories: [ + 'c/d', + { + repository: 'e/f', + repoField: 'g', + }, + ], + }; + it('massages string repos', () => { + expect(configParser.getRepoConfig(config, 0)).toMatchSnapshot(); + }); + it('handles object repos', () => { + expect(configParser.getRepoConfig(config, 1)).toMatchSnapshot(); }); }); }); diff --git a/test/helpers/changelog.spec.js b/test/helpers/changelog.spec.js index 4eb6d6ed3a..81f3a7d3e8 100644 --- a/test/helpers/changelog.spec.js +++ b/test/helpers/changelog.spec.js @@ -1,31 +1,38 @@ const changelog = require('changelog'); const changelogHelper = require('../../lib/helpers/changelog'); +const bunyan = require('bunyan'); + +const logger = bunyan.createLogger({ + name: 'test', + stream: process.stdout, + level: 'fatal', +}); jest.mock('changelog'); describe('helpers/changelog', () => { - describe('changelogHelper.getChangeLog(depName, fromVersion, newVersion)', () => { + describe('changelogHelper.getChangeLog(depName, fromVersion, newVersion, logger)', () => { it('returns empty if no fromVersion', async () => { expect( - await changelogHelper.getChangeLog('renovate', null, '1.0.0') + await changelogHelper.getChangeLog('renovate', null, '1.0.0', logger) ).toBe('No changelog available'); }); it('returns empty if fromVersion equals newVersion', async () => { expect( - await changelogHelper.getChangeLog('renovate', '1.0.0', '1.0.0') + await changelogHelper.getChangeLog('renovate', '1.0.0', '1.0.0', logger) ).toBe('No changelog available'); }); it('returns empty if generated json is null', async () => { changelog.generate.mockReturnValueOnce(null); expect( - await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0') + await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0', logger) ).toBe('No changelog available'); }); it('returns header if generated markdown is valid', async () => { changelog.generate.mockReturnValueOnce({}); changelog.markdown.mockReturnValueOnce('dummy'); expect( - await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0') + await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0', logger) ).toBe('### Changelog\n\ndummy'); }); it('returns empty if error thrown', async () => { @@ -33,7 +40,7 @@ describe('helpers/changelog', () => { throw new Error('foo'); }); expect( - await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0') + await changelogHelper.getChangeLog('renovate', '1.0.0', '2.0.0', logger) ).toBe('No changelog available'); }); }); diff --git a/test/helpers/logger/__snapshots__/config-serializer.spec.js.snap b/test/helpers/logger/__snapshots__/config-serializer.spec.js.snap new file mode 100644 index 0000000000..693839e047 --- /dev/null +++ b/test/helpers/logger/__snapshots__/config-serializer.spec.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`helpers/logger/config-serializer redacts sensitive fields 1`] = ` +Object { + "githubAppKey": "***********", + "nottoken": "b", + "token": "***********", +} +`; + +exports[`helpers/logger/config-serializer replaces functions 1`] = ` +Object { + "api": "[Function]", + "logger": "[Function]", + "nottoken": "b", +} +`; diff --git a/test/helpers/logger/__snapshots__/pretty-stdout.spec.js.snap b/test/helpers/logger/__snapshots__/pretty-stdout.spec.js.snap new file mode 100644 index 0000000000..b465c2c873 --- /dev/null +++ b/test/helpers/logger/__snapshots__/pretty-stdout.spec.js.snap @@ -0,0 +1,6 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`helpers/logger/pretty-stdout getDetails(rec) supports a config 1`] = ` +" {\\"a\\": \\"b\\", \\"d\\": [\\"e\\", \\"f\\"]} +" +`; diff --git a/test/helpers/logger/config-serializer.spec.js b/test/helpers/logger/config-serializer.spec.js new file mode 100644 index 0000000000..44baa875ac --- /dev/null +++ b/test/helpers/logger/config-serializer.spec.js @@ -0,0 +1,20 @@ +const configSerializer = require('../../../lib/helpers/logger/config-serializer'); + +describe('helpers/logger/config-serializer', () => { + it('redacts sensitive fields', () => { + const config = { + token: 'a', + nottoken: 'b', + githubAppKey: 'c', + }; + expect(configSerializer(config)).toMatchSnapshot(); + }); + it('replaces functions', () => { + const config = { + api: 'a', + nottoken: 'b', + logger: {}, + }; + expect(configSerializer(config)).toMatchSnapshot(); + }); +}); diff --git a/test/helpers/logger/pretty-stdout.spec.js b/test/helpers/logger/pretty-stdout.spec.js new file mode 100644 index 0000000000..61c5086ede --- /dev/null +++ b/test/helpers/logger/pretty-stdout.spec.js @@ -0,0 +1,62 @@ +const prettyStdout = require('../../../lib/helpers/logger/pretty-stdout'); +const chalk = require('chalk'); + +describe('helpers/logger/pretty-stdout', () => { + describe('getMeta(rec)', () => { + it('returns empty string if null rec', () => { + expect(prettyStdout.getMeta(null)).toEqual(''); + }); + it('returns empty string if empty rec', () => { + expect(prettyStdout.getMeta({})).toEqual(''); + }); + it('returns empty string if no meta fields', () => { + const rec = { + foo: 'bar', + }; + expect(prettyStdout.getMeta(rec)).toEqual(''); + }); + it('supports single meta', () => { + const rec = { + foo: 'bar', + repository: 'a/b', + }; + expect(prettyStdout.getMeta(rec)).toEqual( + chalk.gray(' (repository=a/b)') + ); + }); + it('supports multi meta', () => { + const rec = { + foo: 'bar', + branch: 'c', + repository: 'a/b', + }; + expect(prettyStdout.getMeta(rec)).toEqual( + chalk.gray(' (repository=a/b, branch=c)') + ); + }); + }); + describe('getDetails(rec)', () => { + it('returns empty string if null rec', () => { + expect(prettyStdout.getDetails(null)).toEqual(''); + }); + it('returns empty string if empty rec', () => { + expect(prettyStdout.getDetails({})).toEqual(''); + }); + it('returns empty string if no meta fields', () => { + const rec = { + foo: 'bar', + }; + expect(prettyStdout.getDetails(rec)).toEqual(''); + }); + it('supports a config', () => { + const rec = { + foo: 'bar', + config: { + a: 'b', + d: ['e', 'f'], + }, + }; + expect(prettyStdout.getDetails(rec)).toMatchSnapshot(); + }); + }); +}); diff --git a/test/helpers/package-json.spec.js b/test/helpers/package-json.spec.js index 6d8befe7b1..a973c30091 100644 --- a/test/helpers/package-json.spec.js +++ b/test/helpers/package-json.spec.js @@ -1,6 +1,13 @@ const fs = require('fs'); const path = require('path'); const packageJson = require('../../lib/helpers/package-json'); +const bunyan = require('bunyan'); + +const logger = bunyan.createLogger({ + name: 'test', + stream: process.stdout, + level: 'fatal', +}); const defaultTypes = [ 'dependencies', @@ -46,14 +53,15 @@ describe('helpers/package-json', () => { extractedDependencies.should.have.length(6); }); }); - describe('.setNewValue(currentFileContent, depType, depName, newVersion)', () => { + describe('.setNewValue(currentFileContent, depType, depName, newVersion, logger)', () => { it('replaces a dependency value', () => { const outputContent = readFixture('outputs/011.json'); const testContent = packageJson.setNewValue( input01Content, 'dependencies', 'cheerio', - '0.22.1' + '0.22.1', + logger ); testContent.should.equal(outputContent); }); @@ -63,7 +71,8 @@ describe('helpers/package-json', () => { input01Content, 'devDependencies', 'angular-touch', - '1.6.1' + '1.6.1', + logger ); testContent.should.equal(outputContent); }); @@ -73,7 +82,8 @@ describe('helpers/package-json', () => { input01Content, 'devDependencies', 'angular-sanitize', - '1.6.1' + '1.6.1', + logger ); testContent.should.equal(outputContent); }); @@ -82,7 +92,8 @@ describe('helpers/package-json', () => { input01Content, 'devDependencies', 'angular-touch', - '1.5.8' + '1.5.8', + logger ); testContent.should.equal(input01Content); }); diff --git a/test/__snapshots__/worker.spec.js.snap b/test/workers/__snapshots__/package-file.spec.js.snap similarity index 64% rename from test/__snapshots__/worker.spec.js.snap rename to test/workers/__snapshots__/package-file.spec.js.snap index 8fe51724e7..0c7271025e 100644 --- a/test/__snapshots__/worker.spec.js.snap +++ b/test/workers/__snapshots__/package-file.spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`worker assignDepConfigs(inputConfig, deps) handles depType config with override 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles depType config with override 1`] = ` Array [ Object { "config": Object { @@ -12,7 +12,7 @@ Array [ ] `; -exports[`worker assignDepConfigs(inputConfig, deps) handles depType config without override 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles depType config without override 1`] = ` Array [ Object { "config": Object { @@ -25,7 +25,7 @@ Array [ ] `; -exports[`worker assignDepConfigs(inputConfig, deps) handles multiple deps 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles multiple deps 1`] = ` Array [ Object { "config": Object { @@ -42,7 +42,7 @@ Array [ ] `; -exports[`worker assignDepConfigs(inputConfig, deps) handles non-regex package name 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles non-regex package name 1`] = ` Array [ Object { "config": Object { @@ -74,7 +74,7 @@ Array [ ] `; -exports[`worker assignDepConfigs(inputConfig, deps) handles package config 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles package config 1`] = ` Array [ Object { "config": Object { @@ -88,7 +88,7 @@ Array [ ] `; -exports[`worker assignDepConfigs(inputConfig, deps) handles regex package pattern 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles regex package pattern 1`] = ` Array [ Object { "config": Object { @@ -126,7 +126,7 @@ Array [ ] `; -exports[`worker assignDepConfigs(inputConfig, deps) handles regex wildcard package pattern 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles regex wildcard package pattern 1`] = ` Array [ Object { "config": Object { @@ -161,7 +161,7 @@ Array [ ] `; -exports[`worker assignDepConfigs(inputConfig, deps) handles string deps 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) handles string deps 1`] = ` Array [ Object { "config": Object { @@ -172,7 +172,7 @@ Array [ ] `; -exports[`worker assignDepConfigs(inputConfig, deps) nested package config overrides depType and general config 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) nested package config overrides depType and general config 1`] = ` Array [ Object { "config": Object { @@ -184,7 +184,7 @@ Array [ ] `; -exports[`worker assignDepConfigs(inputConfig, deps) package config overrides depType and general config 1`] = ` +exports[`packageFileWorker assignDepConfigs(inputConfig, deps) package config overrides depType and general config 1`] = ` Array [ Object { "config": Object { @@ -195,3 +195,21 @@ Array [ }, ] `; + +exports[`packageFileWorker processPackageFile(config) extracts dependencies for each depType 1`] = ` +Array [ + Array [ + Object {}, + Array [ + "dependencies", + "devDependencies", + ], + ], +] +`; + +exports[`packageFileWorker processPackageFile(config) filters dependencies 1`] = ` +Array [ + "a", +] +`; diff --git a/test/workers/__snapshots__/pr.spec.js.snap b/test/workers/__snapshots__/pr.spec.js.snap index fddec193f4..c1af747f56 100644 --- a/test/workers/__snapshots__/pr.spec.js.snap +++ b/test/workers/__snapshots__/pr.spec.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`workers/pr ensurePr(upgrades) should return unmodified existing PR 1`] = `Array []`; +exports[`workers/pr ensurePr(upgrades, logger) should return unmodified existing PR 1`] = `Array []`; diff --git a/test/workers/branch.spec.js b/test/workers/branch.spec.js index 9faf69c6fa..d21e20dafe 100644 --- a/test/workers/branch.spec.js +++ b/test/workers/branch.spec.js @@ -1,9 +1,18 @@ const branchWorker = require('../../lib/workers/branch'); +const prWorker = require('../../lib/workers/pr'); const npmHelper = require('../../lib/helpers/npm'); const yarnHelper = require('../../lib/helpers/yarn'); const defaultConfig = require('../../lib/config/defaults').getConfig(); const packageJsonHelper = require('../../lib/helpers/package-json'); +const bunyan = require('bunyan'); + +const logger = bunyan.createLogger({ + name: 'test', + stream: process.stdout, + level: 'fatal', +}); + jest.mock('../../lib/helpers/yarn'); jest.mock('../../lib/helpers/package-json'); @@ -304,4 +313,48 @@ describe('workers/branch', () => { expect(config.api.commitFilesToBranch.mock.calls.length).toBe(0); }); }); + describe('updateBranch(upgrades)', () => { + let config; + beforeEach(() => { + config = Object.assign({}, defaultConfig); + config.api = { + checkForClosedPr: jest.fn(), + }; + branchWorker.ensureBranch = jest.fn(); + prWorker.ensurePr = jest.fn(); + }); + it('returns immediately if closed PR found', async () => { + config.api.checkForClosedPr.mockReturnValue(true); + await branchWorker.updateBranch([config], logger); + expect(branchWorker.ensureBranch.mock.calls.length).toBe(0); + }); + it('does not return immediately if recreateClosed true', async () => { + config.api.checkForClosedPr.mockReturnValue(true); + config.recreateClosed = true; + await branchWorker.updateBranch([config], logger); + expect(branchWorker.ensureBranch.mock.calls.length).toBe(1); + }); + it('pins', async () => { + config.upgradeType = 'pin'; + await branchWorker.updateBranch([config], logger); + expect(branchWorker.ensureBranch.mock.calls.length).toBe(1); + }); + it('majors', async () => { + config.upgradeType = 'major'; + await branchWorker.updateBranch([config], logger); + expect(branchWorker.ensureBranch.mock.calls.length).toBe(1); + }); + it('minors', async () => { + config.upgradeType = 'minor'; + await branchWorker.updateBranch([config], logger); + expect(branchWorker.ensureBranch.mock.calls.length).toBe(1); + }); + it('handles errors', async () => { + config.api.checkForClosedPr = jest.fn(() => { + throw new Error('oops'); + }); + await branchWorker.updateBranch([config], logger); + expect(branchWorker.ensureBranch.mock.calls.length).toBe(0); + }); + }); }); diff --git a/test/index.spec.js b/test/workers/index.spec.js similarity index 76% rename from test/index.spec.js rename to test/workers/index.spec.js index 9d45856e25..5cacbbdab3 100644 --- a/test/index.spec.js +++ b/test/workers/index.spec.js @@ -1,4 +1,4 @@ -require('../lib/index'); +require('../../lib/workers/index'); it('placeholder', () => { // TODO: write tests for this module - this is here so the file shows up in coverage diff --git a/test/worker.spec.js b/test/workers/package-file.spec.js similarity index 56% rename from test/worker.spec.js rename to test/workers/package-file.spec.js index c4b6381835..0fc62553f0 100644 --- a/test/worker.spec.js +++ b/test/workers/package-file.spec.js @@ -1,81 +1,29 @@ -const worker = require('../lib/worker'); -const branchWorker = require('../lib/workers/branch'); -const prWorker = require('../lib/workers/pr'); -const defaultConfig = require('../lib/config/defaults').getConfig(); -const npmApi = require('../lib/api/npm'); -const versionsHelper = require('../lib/helpers/versions'); +const packageFileWorker = require('../../lib/workers/package-file'); +const npmApi = require('../../lib/api/npm'); +const versionsHelper = require('../../lib/helpers/versions'); +const packageJsonHelper = require('../../lib/helpers/package-json'); +const bunyan = require('bunyan'); -jest.mock('../lib/workers/branch'); -jest.mock('../lib/workers/pr'); -jest.mock('../lib/api/npm'); -jest.mock('../lib/helpers/versions'); +const logger = bunyan.createLogger({ + name: 'test', + stream: process.stdout, + level: 'fatal', +}); -describe('worker', () => { - describe('updateDependency(upgrade)', () => { - let config; - beforeEach(() => { - config = Object.assign({}, defaultConfig); - config.api = { - checkForClosedPr: jest.fn(), - }; - branchWorker.ensureBranch = jest.fn(); - prWorker.ensurePr = jest.fn(); - }); - it('returns immediately if closed PR found', async () => { - config.api.checkForClosedPr.mockReturnValue(true); - await worker.updateBranch([config]); - expect(branchWorker.ensureBranch.mock.calls.length).toBe(0); - }); - it('does not return immediately if recreateClosed true', async () => { - config.api.checkForClosedPr.mockReturnValue(true); - config.recreateClosed = true; - await worker.updateBranch([config]); - expect(branchWorker.ensureBranch.mock.calls.length).toBe(1); - }); - it('pins', async () => { - config.upgradeType = 'pin'; - await worker.updateBranch([config]); - expect(branchWorker.ensureBranch.mock.calls.length).toBe(1); - }); - it('majors', async () => { - config.upgradeType = 'major'; - await worker.updateBranch([config]); - expect(branchWorker.ensureBranch.mock.calls.length).toBe(1); - }); - it('minors', async () => { - config.upgradeType = 'minor'; - await worker.updateBranch([config]); - expect(branchWorker.ensureBranch.mock.calls.length).toBe(1); - }); - it('handles errors', async () => { - config.api.checkForClosedPr = jest.fn(() => { - throw new Error('oops'); - }); - await worker.updateBranch([config]); - expect(branchWorker.ensureBranch.mock.calls.length).toBe(0); - }); - }); - describe('processUpgrades(upgrades)', () => { - beforeEach(() => { - worker.updateBranch = jest.fn(); - }); - it('handles zero upgrades', async () => { - await worker.processUpgrades([]); - expect(worker.updateBranch.mock.calls.length).toBe(0); - }); - it('handles non-zero upgrades', async () => { - await worker.processUpgrades([{ branchName: 'a' }, { branchName: 'b' }]); - expect(worker.updateBranch.mock.calls.length).toBe(2); - }); - }); +jest.mock('../../lib/workers/branch'); +jest.mock('../../lib/workers/pr'); +jest.mock('../../lib/api/npm'); +jest.mock('../../lib/helpers/versions'); + +describe('packageFileWorker', () => { describe('findUpgrades(dependencies, config)', () => { let config; beforeEach(() => { config = {}; - worker.updateBranch = jest.fn(); + packageFileWorker.updateBranch = jest.fn(); }); it('handles null', async () => { - const allUpgrades = await worker.findUpgrades([], config); + const allUpgrades = await packageFileWorker.findUpgrades([], config); expect(allUpgrades).toMatchObject([]); }); it('handles one dep', async () => { @@ -86,9 +34,21 @@ describe('worker', () => { const upgrade = { newVersion: '1.1.0' }; npmApi.getDependency = jest.fn(() => ({})); versionsHelper.determineUpgrades = jest.fn(() => [upgrade]); - const allUpgrades = await worker.findUpgrades([dep], config); + const allUpgrades = await packageFileWorker.findUpgrades([dep], config); expect(allUpgrades).toMatchObject([Object.assign({}, dep, upgrade)]); }); + it('handles no return', async () => { + const dep = { + depName: 'foo', + currentVersion: '1.0.0', + }; + const upgrade = { newVersion: '1.1.0' }; + npmApi.getDependency = jest.fn(() => ({})); + npmApi.getDependency.mockReturnValueOnce(null); + versionsHelper.determineUpgrades = jest.fn(() => [upgrade]); + const allUpgrades = await packageFileWorker.findUpgrades([dep], config); + expect(allUpgrades).toMatchObject([]); + }); it('handles no upgrades', async () => { const dep = { depName: 'foo', @@ -96,7 +56,7 @@ describe('worker', () => { }; npmApi.getDependency = jest.fn(() => ({})); versionsHelper.determineUpgrades = jest.fn(() => []); - const allUpgrades = await worker.findUpgrades([dep], config); + const allUpgrades = await packageFileWorker.findUpgrades([dep], config); expect(allUpgrades).toMatchObject([]); }); }); @@ -108,7 +68,7 @@ describe('worker', () => { deps = []; }); it('handles empty deps', () => { - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchObject([]); }); it('handles string deps', () => { @@ -117,7 +77,7 @@ describe('worker', () => { deps.push({ depName: 'a', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); it('handles multiple deps', () => { @@ -128,7 +88,7 @@ describe('worker', () => { deps.push({ depName: 'b', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); it('handles depType config without override', () => { @@ -143,7 +103,7 @@ describe('worker', () => { depName: 'a', depType: 'dependencies', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); it('handles depType config with override', () => { @@ -158,7 +118,7 @@ describe('worker', () => { depName: 'a', depType: 'dependencies', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); it('handles package config', () => { @@ -172,7 +132,7 @@ describe('worker', () => { deps.push({ depName: 'a', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); it('package config overrides depType and general config', () => { @@ -193,7 +153,7 @@ describe('worker', () => { depName: 'a', depType: 'dependencies', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); it('nested package config overrides depType and general config', () => { @@ -214,7 +174,7 @@ describe('worker', () => { depName: 'a', depType: 'dependencies', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); it('handles regex package pattern', () => { @@ -237,7 +197,7 @@ describe('worker', () => { deps.push({ depName: 'also-eslint', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); it('handles regex wildcard package pattern', () => { @@ -260,7 +220,7 @@ describe('worker', () => { deps.push({ depName: 'also-eslint', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); it('handles non-regex package name', () => { @@ -283,18 +243,24 @@ describe('worker', () => { deps.push({ depName: 'also-eslint', }); - const updatedDeps = worker.assignDepConfigs(config, deps); + const updatedDeps = packageFileWorker.assignDepConfigs(config, deps); expect(updatedDeps).toMatchSnapshot(); }); }); describe('getDepTypeConfig(depTypes, depTypeName)', () => { it('handles empty depTypes', () => { - const depTypeConfig = worker.getDepTypeConfig([], 'dependencies'); + const depTypeConfig = packageFileWorker.getDepTypeConfig( + [], + 'dependencies' + ); expect(depTypeConfig).toMatchObject({}); }); it('handles all strings', () => { const depTypes = ['dependencies', 'devDependencies']; - const depTypeConfig = worker.getDepTypeConfig(depTypes, 'dependencies'); + const depTypeConfig = packageFileWorker.getDepTypeConfig( + depTypes, + 'dependencies' + ); expect(depTypeConfig).toMatchObject({}); }); it('handles missed object', () => { @@ -305,7 +271,10 @@ describe('worker', () => { foo: 'bar', }, ]; - const depTypeConfig = worker.getDepTypeConfig(depTypes, 'dependencies'); + const depTypeConfig = packageFileWorker.getDepTypeConfig( + depTypes, + 'dependencies' + ); expect(depTypeConfig).toMatchObject({}); }); it('handles hit object', () => { @@ -316,11 +285,72 @@ describe('worker', () => { }, 'devDependencies', ]; - const depTypeConfig = worker.getDepTypeConfig(depTypes, 'dependencies'); + const depTypeConfig = packageFileWorker.getDepTypeConfig( + depTypes, + 'dependencies' + ); const expectedResult = { foo: 'bar', }; expect(depTypeConfig).toMatchObject(expectedResult); }); }); + describe('processPackageFile(config)', () => { + let config; + beforeEach(() => { + packageFileWorker.assignDepConfigs = jest.fn(() => []); + packageFileWorker.findUpgrades = jest.fn(() => []); + packageJsonHelper.extractDependencies = jest.fn(() => []); + config = require('../../lib/config/defaults').getConfig(); + config.api = { + getFileJson: jest.fn(() => ({})), + }; + config.logger = logger; + }); + it('returns empty array if no package content', async () => { + config.api.getFileJson.mockReturnValueOnce(null); + const res = await packageFileWorker.processPackageFile(config); + expect(res).toEqual([]); + }); + it('returns empty array if config disabled', async () => { + config.api.getFileJson.mockReturnValueOnce({ + renovate: { + enabled: false, + }, + }); + const res = await packageFileWorker.processPackageFile(config); + expect(res).toEqual([]); + }); + it('extracts dependencies for each depType', async () => { + config.depTypes = [ + 'dependencies', + { + depType: 'devDependencies', + foo: 'bar', + }, + ]; + const res = await packageFileWorker.processPackageFile(config); + expect(res).toEqual([]); + expect( + packageJsonHelper.extractDependencies.mock.calls + ).toMatchSnapshot(); + }); + it('filters dependencies', async () => { + packageJsonHelper.extractDependencies.mockReturnValueOnce([ + { + depName: 'a', + }, + ]); + packageFileWorker.assignDepConfigs.mockReturnValueOnce(['a']); + packageFileWorker.findUpgrades.mockReturnValueOnce(['a']); + const res = await packageFileWorker.processPackageFile(config); + expect(res).toHaveLength(1); + expect(res).toMatchSnapshot(); + }); + it('maintains yarn.lock', async () => { + config.maintainYarnLock = true; + const res = await packageFileWorker.processPackageFile(config); + expect(res).toHaveLength(1); + }); + }); }); diff --git a/test/workers/pr.spec.js b/test/workers/pr.spec.js index 19d18db1be..5383334116 100644 --- a/test/workers/pr.spec.js +++ b/test/workers/pr.spec.js @@ -2,6 +2,14 @@ const prWorker = require('../../lib/workers/pr'); const changelogHelper = require('../../lib/helpers/changelog'); const defaultConfig = require('../../lib/config/defaults').getConfig(); +const bunyan = require('bunyan'); + +const logger = bunyan.createLogger({ + name: 'test', + stream: process.stdout, + level: 'fatal', +}); + jest.mock('../../lib/helpers/changelog'); changelogHelper.getChangeLog = jest.fn(); changelogHelper.getChangeLog.mockReturnValue('Mocked changelog'); @@ -27,7 +35,7 @@ changelogHelper.getChangeLogJSON.mockReturnValue({ }); describe('workers/pr', () => { - describe('checkAutoMerge(pr, config)', () => { + describe('checkAutoMerge(pr, config, logger)', () => { let config; let pr; beforeEach(() => { @@ -43,38 +51,38 @@ describe('workers/pr', () => { }; }); it('should not automerge if not configured', async () => { - await prWorker.checkAutoMerge(pr, config); + await prWorker.checkAutoMerge(pr, config, logger); expect(config.api.mergePr.mock.calls.length).toBe(0); }); it('should automerge if enabled and pr is mergeable', async () => { config.automergeEnabled = true; pr.mergeable = true; config.api.getBranchStatus.mockReturnValueOnce('success'); - await prWorker.checkAutoMerge(pr, config); + await prWorker.checkAutoMerge(pr, config, logger); expect(config.api.mergePr.mock.calls.length).toBe(1); }); it('should not automerge if enabled and pr is mergeable but branch status is not success', async () => { config.automergeEnabled = true; pr.mergeable = true; config.api.getBranchStatus.mockReturnValueOnce('pending'); - await prWorker.checkAutoMerge(pr, config); + await prWorker.checkAutoMerge(pr, config, logger); expect(config.api.mergePr.mock.calls.length).toBe(0); }); it('should not automerge if enabled and pr is mergeable but unstable', async () => { config.automergeEnabled = true; pr.mergeable = true; pr.mergeable_state = 'unstable'; - await prWorker.checkAutoMerge(pr, config); + await prWorker.checkAutoMerge(pr, config, logger); expect(config.api.mergePr.mock.calls.length).toBe(0); }); it('should not automerge if enabled and pr is unmergeable', async () => { config.automergeEnabled = true; pr.mergeable = false; - await prWorker.checkAutoMerge(pr, config); + await prWorker.checkAutoMerge(pr, config, logger); expect(config.api.mergePr.mock.calls.length).toBe(0); }); }); - describe('ensurePr(upgrades)', () => { + describe('ensurePr(upgrades, logger)', () => { let config; let existingPr; beforeEach(() => { @@ -106,45 +114,45 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren config.api.getBranchPr = jest.fn(() => { throw new Error('oops'); }); - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toBe(null); }); it('should return null if waiting for success', async () => { config.api.getBranchStatus = jest.fn(() => 'failed'); config.prCreation = 'status-success'; - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toBe(null); }); it('should create PR if success', async () => { config.api.getBranchStatus = jest.fn(() => 'success'); config.api.getBranchPr = jest.fn(); config.prCreation = 'status-success'; - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); }); it('should return null if waiting for not pending', async () => { config.api.getBranchStatus = jest.fn(() => 'pending'); config.prCreation = 'not-pending'; - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toBe(null); }); it('should create PR if no longer pending', async () => { config.api.getBranchStatus = jest.fn(() => 'failed'); config.api.getBranchPr = jest.fn(); config.prCreation = 'not-pending'; - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); }); it('should create new branch if none exists', async () => { config.api.getBranchPr = jest.fn(); - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); }); it('should add labels to new PR', async () => { config.api.getBranchPr = jest.fn(); config.api.addLabels = jest.fn(); config.labels = ['foo']; - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); expect(config.api.addLabels.mock.calls.length).toBe(1); }); @@ -152,7 +160,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren config.api.getBranchPr = jest.fn(); config.api.addLabels = jest.fn(); config.labels = []; - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); expect(config.api.addLabels.mock.calls.length).toBe(0); }); @@ -162,7 +170,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren config.api.addReviewers = jest.fn(); config.assignees = ['bar']; config.reviewers = ['baz']; - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); expect(config.api.addAssignees.mock.calls.length).toBe(1); expect(config.api.addReviewers.mock.calls.length).toBe(1); @@ -174,7 +182,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren config.assignees = ['bar']; config.reviewers = ['baz']; config.automergeEnabled = true; - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); expect(config.api.addAssignees.mock.calls.length).toBe(0); expect(config.api.addReviewers.mock.calls.length).toBe(0); @@ -185,7 +193,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren config.newVersion = '1.1.0'; config.api.getBranchPr = jest.fn(() => existingPr); config.api.updatePr = jest.fn(); - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(config.api.updatePr.mock.calls).toMatchSnapshot(); expect(config.api.updatePr.mock.calls.length).toBe(0); expect(pr).toMatchObject(existingPr); @@ -196,7 +204,7 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren config.newVersion = '1.2.0'; config.api.getBranchPr = jest.fn(() => existingPr); config.api.updatePr = jest.fn(); - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); const updatedPr = Object.assign(existingPr, { body: 'This Pull Request updates dependency dummy from version `1.0.0` to `1.2.0`\n\nNo changelog available', @@ -208,14 +216,14 @@ This PR has been generated by [Renovate Bot](https://keylocation.sg/our-tech/ren config.automergeType = 'branch-push'; config.api.getBranchStatus.mockReturnValueOnce('failure'); config.api.getBranchPr = jest.fn(); - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toMatchObject({ displayNumber: 'New Pull Request' }); }); it('should return null if branch automerging not failed', async () => { config.automergeEnabled = true; config.automergeType = 'branch-push'; config.api.getBranchStatus.mockReturnValueOnce('pending'); - const pr = await prWorker.ensurePr([config]); + const pr = await prWorker.ensurePr([config], logger); expect(pr).toBe(null); }); }); diff --git a/test/workers/repository.spec.js b/test/workers/repository.spec.js new file mode 100644 index 0000000000..f99ddc6252 --- /dev/null +++ b/test/workers/repository.spec.js @@ -0,0 +1,26 @@ +const repositoryWorker = require('../../lib/workers/repository'); +const branchWorker = require('../../lib/workers/branch'); + +jest.mock('../../lib/workers/branch'); +jest.mock('../../lib/workers/pr'); +jest.mock('../../lib/api/npm'); +jest.mock('../../lib/helpers/versions'); + +describe('repositoryWorker', () => { + describe('processUpgrades(upgrades)', () => { + beforeEach(() => { + repositoryWorker.updateBranch = jest.fn(); + }); + it('handles zero upgrades', async () => { + // await repositoryWorker.processUpgrades([]); + expect(branchWorker.updateBranch.mock.calls.length).toBe(0); + }); + it('handles non-zero upgrades', async () => { + await repositoryWorker.processUpgrades([ + { branchName: 'a' }, + { branchName: 'b' }, + ]); + expect(branchWorker.updateBranch.mock.calls.length).toBe(2); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 3678bca9ec..d589e045d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3475,6 +3475,10 @@ tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" +traverse@0.6.6: + version "0.6.6" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" + trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" -- GitLab