From b4aaa65b5cec2008620022719ddfb0fc83975c43 Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@keylocation.sg> Date: Sat, 7 Jan 2017 08:22:48 +0100 Subject: [PATCH] Move code into github and npm helper libraries commit 8e84875bd5f7e4584d707d88d6850565bb02c79c Author: Rhys Arkins <rhys@keylocation.sg> Date: Sat Jan 7 08:22:21 2017 +0100 Synchronous commit 0f24ea192bcf54aae1264e91a4b6eb98fea55448 Author: Rhys Arkins <rhys@keylocation.sg> Date: Sat Jan 7 07:12:20 2017 +0100 externalise more npm commit 458d60975fc967f1373c81cd0fa28a9717dd9b0b Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 15:45:08 2017 +0100 Externalise npm commit 5d4f39e72d2977af1fec12d7a0a39d3877e4ad02 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 15:35:16 2017 +0100 Remove ghGot commit 06898801c1e591d6db9e6ac1e565233af5e9be7e Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 15:34:43 2017 +0100 Externalise PR functions commit 0b0e0f781b3384ad57a1df3df7d1089b2c72079a Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 15:34:25 2017 +0100 Enable verbose commit 4cebf1e0a80d7e14b9704c5fd7e5d0b036b9661a Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 14:23:12 2017 +0100 verbose commit 5a984b91e099cccb5c9dff857a6be07b3b4dedd5 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 14:22:59 2017 +0100 Change default branch naming commit ab9bc952c81d16be9be57227382dff8d05e73f54 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 13:05:08 2017 +0100 Fix branch matching commit eeecf17e196245964aed5247cf1703619d42b0d4 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 11:15:16 2017 +0100 Update message commit d27b345c5eb51dcb7e32b903beafe0728e24bfdb Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 11:09:39 2017 +0100 Refactor file write commit 7f12ef69f456ecd064be5d9851157131222f7700 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 10:59:24 2017 +0100 Refactor writeFile commit 8c7cc9e6a6c7e398aa60cb828c16ff51f36f2efa Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 10:39:27 2017 +0100 Refactor getFile commit b4338ade6d29b830ead657267248c93216c2f91d Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 10:15:02 2017 +0100 refactor commit dc4aeb39dad367844836da7f93e9f167864f6030 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 10:14:34 2017 +0100 createBranch commit d6a357f609de55d7b934652f30592219391a9884 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 10:04:04 2017 +0100 Add createBranch commit 11ba4e9f6c2153d7b783670944570cb4968ff718 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 07:27:08 2017 +0100 Rename commit 7a4be0fde0e070e2149bc4c34397c4903096ac51 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 07:17:31 2017 +0100 Externalise some github functions commit e393e92bcc9cb548fac3637644b0330a136f3611 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 07:17:19 2017 +0100 Fix error message commit 59fb50656d84491780bc31bab4cb9263a7912c03 Author: Rhys Arkins <rhys@keylocation.sg> Date: Fri Jan 6 07:16:59 2017 +0100 Improve error checks commit bc44b3a0d820ab5756c3b3c746402329e5b52703 Author: Rhys Arkins <rhys@keylocation.sg> Date: Thu Jan 5 15:34:04 2017 +0100 Make base branch configurable commit b9d31776814723d991a226d1ca1b2f39d0d2af85 Author: Rhys Arkins <rhys@keylocation.sg> Date: Thu Jan 5 15:33:44 2017 +0100 Reorder early lines commit b75f9f25cfb86f029b73445aae67b7889ff09b3e Author: Rhys Arkins <rhys@keylocation.sg> Date: Thu Jan 5 15:26:47 2017 +0100 Error if RENOVATE_TOKEN is undefined Closes #11 commit 34e13a70326a71b3ee7f18c12ec3de55b78bcaa1 Author: Rhys Arkins <rhys@keylocation.sg> Date: Thu Jan 5 14:43:42 2017 +0100 arrow functions commit 6006db2deae887938bc20a07c93d1a59bd8cd74e Author: Rhys Arkins <rhys@keylocation.sg> Date: Thu Jan 5 14:39:30 2017 +0100 Refactor templates --- src/config.js | 21 +++++ src/github.js | 119 ++++++++++++++++++++++++++ src/index.js | 227 +++++++++++++++++++++----------------------------- src/npm.js | 37 ++++++++ 4 files changed, 274 insertions(+), 130 deletions(-) create mode 100644 src/config.js create mode 100644 src/github.js create mode 100644 src/npm.js diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000000..d88a4dcb7c --- /dev/null +++ b/src/config.js @@ -0,0 +1,21 @@ +module.exports = { + verbose: false, + baseBranch: 'master', + templates: { + branchName: (params) => { + return `renovate/${params.depName}-${params.nextVersionMajor}.x`; + }, + commitMessage: (params) => { + return `Upgrade dependency ${params.depName} to version ${params.nextVersion}`; + }, + prBody: (params) => { + return `This Pull Request updates dependency ${params.depName} from version ${params.currentVersion} to ${params.nextVersion}.`; + }, + prTitleMajor: (params) => { + return `Upgrade dependency ${params.depName} to version ${params.nextVersionMajor}.x`; + }, + prTitleMinor: (params) => { + return `Upgrade dependency ${params.depName} to version ${params.nextVersion}`; + }, + } +}; diff --git a/src/github.js b/src/github.js new file mode 100644 index 0000000000..109c9858d3 --- /dev/null +++ b/src/github.js @@ -0,0 +1,119 @@ +const ghGot = require('gh-got'); + +var config = {}; + +module.exports = { + // Initialize GitHub by getting base branch SHA + init: function(token, repoName, baseBranch, verbose = false) { + config.token = token; + config.repoName = repoName; + config.baseBranch = baseBranch; + config.verbose = verbose; + + config.userName = repoName.split('/')[0]; + + return ghGot(`repos/${config.repoName}/git/refs/head`, {token: config.token}).then(res => { + // First, get the SHA for base branch + res.body.forEach(function(branch) { + // Loop through all branches because the base branch may not be the first + if (branch.ref === `refs/heads/${config.baseBranch}`) { + // This is the SHA we will create new branches from + config.baseSHA = branch.object.sha; + } + }); + }).catch(function(err) { + console.log('init error: ' + err); + }); + }, + createBranch: function(branchName) { + return ghGot.post(`repos/${config.repoName}/git/refs`, { + token: config.token, + body: { + ref: `refs/heads/${branchName}`, + sha: config.baseSHA, + }, + }); + }, + createPr: function(branchName, title, body) { + return ghGot.post(`repos/${config.repoName}/pulls`, { + token: config.token, + body: { + title: title, + head: branchName, + base: config.baseBranch, + body: body, + } + }); + }, + checkPrExists(branchName, prTitle) { + if (config.verbose) { + console.log(`Checking if branch/PR exists: ${branchName} / ${prTitle}`); + } + return ghGot(`repos/${config.repoName}/pulls?state=closed&head=${config.userName}:${branchName}`, { token: config.token }) + .then(res => { + if (config.verbose) { + console.log(`Got ${res.body.length} results for ${branchName}`); + } + let prAlreadyExists = false; + res.body.forEach(function(result) { + if (result.title === prTitle && result.head.label === `${config.userName}:${branchName}`) { + prAlreadyExists = true; + } + }); + if (config.verbose) { + if (prAlreadyExists) { + console.log(`PR already exists for ${branchName}`); + } else { + console.log(`PR doesn't exist for ${branchName}`); + } + } + return prAlreadyExists; + }).catch((err) => { + console.error('Error checking if PR already existed'); + }); + }, + getFile: function(filePath, branchName) { + branchName = branchName || config.baseBranch; + return ghGot(`repos/${config.repoName}/contents/${filePath}?ref=${branchName}`, { + token: config.token, + }); + }, + getFileContents: function(filePath, branchName) { + return this.getFile(filePath, branchName).then(res => { + return JSON.parse(new Buffer(res.body.content, 'base64').toString()); + }); + }, + getPrNo: function(branchName) { + return ghGot(`repos/${config.repoName}/pulls?base=${config.baseBranch}&head=${config.userName}:${branchName}`, { + token: config.token, + }).then(res => { + let prNo = 0; + res.body.forEach(function(result) { + if (result.state === 'open' && result.head.label === `${config.userName}:${branchName}`) { + prNo = result.number; + } + }); + return prNo; + }); + }, + writeFile: function(branchName, oldFileSHA, filePath, fileContents, message) { + return ghGot.put(`repos/${config.repoName}/contents/${filePath}`, { + token: config.token, + body: { + branch: branchName, + sha: oldFileSHA, + message: message, + content: new Buffer(fileContents).toString('base64') + } + }); + }, + updatePr: function(prNo, title, body) { + return ghGot.patch(`repos/${config.repoName}/pulls/${prNo}`, { + token: config.token, + body: { + title: title, + body: body, + }, + }); + }, +}; diff --git a/src/index.js b/src/index.js index 374ca1f9c2..1b975ea056 100644 --- a/src/index.js +++ b/src/index.js @@ -1,138 +1,127 @@ -const ghGot = require('gh-got'); -const got = require('got'); const semver = require('semver'); const stable = require('semver-stable'); +const config = require('./config'); +const github = require('./github'); +const npm = require('./npm'); + const token = process.env.RENOVATE_TOKEN; +// token must be defined +if (typeof token === 'undefined') { + console.error('Error: Environment variable RENOVATE_TOKEN must be defined'); + process.exit(1); +} + +if (process.argv.length < 3 || process.argv.length > 4) { + console.error('Error: You must specify the GitHub repository and optionally path.'); + console.log('Example: node src singapore/renovate'); + console.log('Example: node src foo/bar baz/package.json'); + process.exit(1); +} + +// Process command line arguments const repoName = process.argv[2]; const userName = repoName.split('/')[0]; const packageFile = process.argv[3] || 'package.json'; -let masterSHA; -let masterPackageJson; +npm.init(config.verbose); -ghGot(`repos/${repoName}/git/refs/head`, {token: token}).then(res => { - // First, get the SHA for master branch - res.body.forEach(function(branch) { - // Loop through all branches because master may not be the first - if (branch.ref === 'refs/heads/master') { - // This is the SHA we will create new branches from - masterSHA = branch.object.sha; - } - }); - // Now, retrieve the master package.json - ghGot(`repos/${repoName}/contents/${packageFile}`, {token: token}).then(res => { - masterPackageJson = JSON.parse(new Buffer(res.body.content, 'base64').toString()); - // Iterate through dependencies and then devDependencies - return iterateDependencies('dependencies') - .then(() => iterateDependencies('devDependencies')); - }).catch(err => { - console.log('Error reading master package.json'); - }); +let basePackageJson; + +github.init(token, repoName, config.baseBranch, config.verbose).then(() => { + return github.getFileContents(packageFile); +}).then((packageContents) => { + basePackageJson = packageContents; + return iterateDependencies('dependencies'); +}).then(() => { + iterateDependencies('devDependencies'); +}).catch(err => { + console.log('Error: ' + err); }); function iterateDependencies(depType) { - const deps = masterPackageJson[depType]; + const deps = basePackageJson[depType]; if (!deps) { return; } + console.log(`Checking ${Object.keys(deps).length} ${depType}`); return Object.keys(deps).reduce((total, depName) => { return total.then(() => { + if (config.verbose) { + console.log(' * ' + depName); + } const currentVersion = deps[depName].replace(/[^\d.]/g, ''); - if (!semver.valid(currentVersion)) { - console.log('Invalid current version'); + console.log(`${depName}: Invalid current version`); return; } - // supports scoped packages, e.g. @user/package - return got(`https://registry.npmjs.org/${depName.replace('/', '%2F')}`, { json: true }) - .then(res => { - let allUpgrades = {}; - Object.keys(res.body['versions']).forEach(function(version) { - if (stable.is(currentVersion) && !stable.is(version)) { - return; - } - if (semver.gt(version, currentVersion)) { - var thisMajor = semver.major(version); - if (!allUpgrades[thisMajor] || semver.gt(version, allUpgrades[thisMajor])) { - allUpgrades[thisMajor] = version; - } - } - }); - - let upgradePromises = []; - - Object.keys(allUpgrades).forEach(function(upgrade) { - const nextVersion = allUpgrades[upgrade]; - upgradePromises.push(updateDependency(depType, depName, currentVersion, nextVersion)); + return npm.getDependencyUpgrades(depName, currentVersion) + .then(allUpgrades => { + if (config.verbose) { + console.log(`All upgrades for ${depName}: ${JSON.stringify(allUpgrades)}`); + } + return Object.keys(allUpgrades).reduce((promiseChain, upgrade) => { + return promiseChain.then(() => { + return updateDependency(depType, depName, currentVersion, allUpgrades[upgrade]); }); - - return Promise.all(upgradePromises); - }); + }, Promise.resolve()); + }); }); }, Promise.resolve()); } function updateDependency(depType, depName, currentVersion, nextVersion) { const nextVersionMajor = semver.major(nextVersion); - const branchName = `upgrade/${depName}-${nextVersionMajor}.x`; - let prName = ''; + const branchName = config.templates.branchName({depType, depName, currentVersion, nextVersion, nextVersionMajor}); + let prTitle = ''; if (nextVersionMajor > semver.major(currentVersion)) { - prName = `Upgrade dependency ${depName} to version ${nextVersionMajor}.x`; - // Check if PR was already closed previously - ghGot(`repos/${repoName}/pulls?state=closed&head=${userName}:${branchName}`, { token: token }) - .then(res => { - if (res.body.length > 0) { - console.log(`Dependency ${depName} upgrade to ${nextVersionMajor}.x PR already existed, so skipping`); - } else { - writeUpdates(depType, depName, branchName, prName, currentVersion, nextVersion); - } - }); + prTitle = config.templates.prTitleMajor({ depType, depName, currentVersion, nextVersion, nextVersionMajor }); } else { - prName = `Upgrade dependency ${depName} to version ${nextVersion}`; - writeUpdates(depType, depName, branchName, prName, currentVersion, nextVersion); + prTitle = config.templates.prTitleMinor({ depType, depName, currentVersion, nextVersion, nextVersionMajor }); } + // Check if same PR already exists or existed + return github.checkPrExists(branchName, prTitle).then((prExisted) => { + if (!prExisted) { + return writeUpdates(depType, depName, branchName, prTitle, currentVersion, nextVersion); + } else { + console.log(`${depName}: Skipping due to existing PR found.`); + } + }); } -function writeUpdates(depType, depName, branchName, prName, currentVersion, nextVersion) { - const commitMessage = `Upgrade dependency ${depName} to version ${nextVersion}`; - const prBody = `This Pull Request updates dependency ${depName} from version ${currentVersion} to ${nextVersion}.`; - // Try to create branch - const body = { - ref: `refs/heads/${branchName}`, - sha: masterSHA - }; - ghGot.post(`repos/${repoName}/git/refs`, { - token: token, - body: body - }).catch(error => { +function writeUpdates(depType, depName, branchName, prTitle, currentVersion, nextVersion) { + const prBody = config.templates.prBody({ depName, currentVersion, nextVersion }); + return github.createBranch(branchName).catch(error => { if (error.response.body.message !== 'Reference already exists') { - console.log('Error creating branch' + branchName); + console.log('Error creating branch: ' + branchName); console.log(error.response.body); } }).then(res => { - ghGot(`repos/${repoName}/contents/${packageFile}?ref=${branchName}`, { token: token }) - .then(res => { + if (config.verbose) { + console.log(`Branch exists (${branchName}), now writing file`); + } + return github.getFile(packageFile, branchName).then(res => { const oldFileSHA = res.body.sha; - let branchPackageJson = JSON.parse(new Buffer(res.body.content, 'base64').toString()); - if (branchPackageJson[depType][depName] !== nextVersion) { + let currentFileContent = JSON.parse(new Buffer(res.body.content, 'base64').toString()); + if (currentFileContent[depType][depName] !== nextVersion) { // Branch is new, or needs version updated - console.log(`Dependency ${depName} needs upgrading to ${nextVersion}`); - branchPackageJson[depType][depName] = nextVersion; - branchPackageString = JSON.stringify(branchPackageJson, null, 2) + '\n'; + currentFileContent[depType][depName] = nextVersion; + const newPackageString = JSON.stringify(currentFileContent, null, 2) + '\n'; - ghGot.put(`repos/${repoName}/contents/${packageFile}`, { - token: token, - body: { - branch: branchName, - sha: oldFileSHA, - message: commitMessage, - content: new Buffer(branchPackageString).toString('base64') - } - }).then(res => { - return createOrUpdatePullRequest(branchName, prName, prBody); + var commitMessage = config.templates.commitMessage({ depName, currentVersion, nextVersion }); + + return github.writeFile(branchName, oldFileSHA, packageFile, newPackageString, commitMessage) + .then(() => { + return createOrUpdatePullRequest(branchName, prTitle, prBody); + }) + .catch(err => { + console.error('Error writing new package file for ' + depName); + console.log(err); }); + } else { + // File was up to date. Ensure PR + return createOrUpdatePullRequest(branchName, prTitle, prBody); } }); }) @@ -141,44 +130,22 @@ function writeUpdates(depType, depName, branchName, prName, currentVersion, next }); } -function createOrUpdatePullRequest(branchName, title, body) { - return ghGot.post(`repos/${repoName}/pulls`, { - token: token, - body: { - title: title, - head: branchName, - base: 'master', - body: body, - } - }).then(res => { - console.log('Created Pull Request: ' + title); - }).catch(error => { - if (error.response.body.errors[0].message.indexOf('A pull request already exists') === 0) { - // Pull Request already exists - // Now we need to find the Pull Request number - return ghGot(`repos/${repoName}/pulls?base=master&head=${userName}:${branchName}`, { - token: token, - }).then(res => { - // TODO iterate through list and confirm branch - if (res.body.length !== 1) { - console.error('Could not find matching PR'); - return; - } - const existingPrNo = res.body[0].number; - return ghGot.patch(`repos/${repoName}/pulls/${existingPrNo}`, { - token: token, - body: { - title: title, - body: body, - } - }).then(res => { - console.log('Updated Pull Request: ' + title); - }); +function createOrUpdatePullRequest(branchName, prTitle, prBody) { + return github.getPrNo(branchName).then(prNo => { + if (prNo) { + // PR already exists - update it + // Note: PR might be unchanged, so no log message + return github.updatePr(prNo, prTitle, prBody) + .catch(err => { + console.error('Error: Failed to update Pull Request: ' + prTitle); + console.log(err); }); - } else { - console.log('Error creating Pull Request:'); - console.log(error.response.body); - Promise.reject(); } + return github.createPr(branchName, prTitle, prBody).then(res => { + console.log('Created Pull Request: ' + prTitle); + }).catch(err => { + console.error('Error: Failed to create Pull Request: ' + prTitle); + console.log(err); + }); }); } diff --git a/src/npm.js b/src/npm.js new file mode 100644 index 0000000000..d14aaf4571 --- /dev/null +++ b/src/npm.js @@ -0,0 +1,37 @@ +const got = require('got'); +const semver = require('semver'); +const stable = require('semver-stable'); + +var config = {}; + +module.exports = { + init: function(verbose = false) { + config.verbose = verbose; + }, + getDependency(depName) { + if (config.verbose) { + console.log(`Looking up npm for ${depName}`); + } + // supports scoped packages, e.g. @user/package + return got(`https://registry.npmjs.org/${depName.replace('/', '%2F')}`, { json: true }); + }, + getDependencyUpgrades(depName, currentVersion) { + return this.getDependency(depName).then(res => { + let allUpgrades = {}; + Object.keys(res.body['versions']).forEach(function(version) { + if (stable.is(currentVersion) && !stable.is(version)) { + // Ignore unstable versions, unless the current version is unstable + return; + } + if (semver.gt(version, currentVersion)) { + // Group by major versions + var thisMajor = semver.major(version); + if (!allUpgrades[thisMajor] || semver.gt(version, allUpgrades[thisMajor])) { + allUpgrades[thisMajor] = version; + } + } + }); + return allUpgrades; + }); + }, +}; -- GitLab