From bcc3171245ce776094f39f1f02414875bd10bc16 Mon Sep 17 00:00:00 2001 From: Rhys Arkins <rhys@keylocation.sg> Date: Wed, 4 Jan 2017 18:48:55 +0100 Subject: [PATCH] Refactor to use GitHub API instead of git Closes #8, Closes #1 commit 02eefd2ec70bf8a07e667d13a4e33dc70dd9db96 Author: Rhys Arkins <rhys@keylocation.sg> Date: Wed Jan 4 18:25:30 2017 +0100 Refactor updates commit e9330e41a3388879ef300f40d8843210c70e2b31 Author: Rhys Arkins <rhys@keylocation.sg> Date: Wed Jan 4 18:25:17 2017 +0100 Improve commenting commit 2feb32f218a83ec765732280af8b0d9e569fb313 Author: Rhys Arkins <rhys@keylocation.sg> Date: Wed Jan 4 13:34:36 2017 +0100 Refactor token input commit 28b4428bae8cdafffe0227e794e8f77a5be2fcfd Author: Rhys Arkins <rhys@keylocation.sg> Date: Wed Jan 4 13:28:09 2017 +0100 Rename files commit 2fe98be1b31b27f625023ffb748f36c3a0eefee6 Author: Rhys Arkins <rhys@keylocation.sg> Date: Wed Jan 4 13:21:52 2017 +0100 Improve error log commit e6f0e691945e561c458147f52b02903ba82373d7 Author: Rhys Arkins <rhys@keylocation.sg> Date: Mon Dec 19 19:20:53 2016 +0100 Support custom package.json path commit 5f971746d3abe2a40b94cae3b8592ec97b21358e Author: Rhys Arkins <rhys@keylocation.sg> Date: Mon Dec 19 19:20:40 2016 +0100 Handle null dependencies or devDependencies commit 9eac59859626bc7d40cacb1e93e645973667208c Author: Rhys Arkins <rhys@keylocation.sg> Date: Mon Dec 19 18:23:14 2016 +0100 Split per branch commit 61d7337e813b86d186511fdb6ad0655b6110942f Author: Rhys Arkins <rhys@keylocation.sg> Date: Mon Dec 19 18:22:59 2016 +0100 Ignore unstable commit d4d8bcf0895046b5d13f8dea93dbb30121f9be7c Author: Rhys Arkins <rhys@keylocation.sg> Date: Mon Dec 19 18:22:10 2016 +0100 Pin commit 4b9306b8072726b2eed74a0f56a4687c865539a4 Author: Rhys Arkins <rhys@keylocation.sg> Date: Mon Dec 19 11:55:47 2016 +0100 Add new --- package.json | 12 +- src/index.js | 388 +++++++++++++++++++++------------------------------ 2 files changed, 166 insertions(+), 234 deletions(-) diff --git a/package.json b/package.json index 0981094d09..19eea08437 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { "private": true, "dependencies": { - "got": "^6.6.3", - "mkdirp": "^0.5.1", - "nodegit": "^0.16.0", - "rimraf": "^2.5.4", - "semver": "^5.3.0" + "gh-got": "5.0.0", + "got": "6.6.3", + "mkdirp": "0.5.1", + "nodegit": "0.16.0", + "rimraf": "2.5.4", + "semver": "5.3.0", + "semver-stable": "2.0.4" } } diff --git a/src/index.js b/src/index.js index ffbba12b6d..6ebf41b201 100644 --- a/src/index.js +++ b/src/index.js @@ -1,252 +1,182 @@ -'use strict'; - -const Git = require('nodegit'); +const ghGot = require('gh-got'); const got = require('got'); const semver = require('semver'); -const fs = require('fs'); -const mkdirp = require('mkdirp'); -const rimraf = require('rimraf'); - -const authorName = 'Renovate Bot'; // commit credentials -const authorEmail = 'renovate-bot@keylocation.sg'; // commit credentials - -const sshPublicKeyPath = `${process.env.HOME}/.ssh/id_rsa.pub`; -const sshPrivateKeyPath = `${process.env.HOME}/.ssh/id_rsa`; - -if (!module.parent) { - // https://github.com/settings/tokens/new - const token = process.argv[2]; - const repoName = process.argv[3]; - let packageFile = process.argv[4]; - - if (!token || !repoName) { - console.error(`Usage: node index.js <token> <repo>`); - process.exit(1); - } - - if (!packageFile) { - packageFile = 'package.json'; +const stable = require('semver-stable'); + +const token = process.env.RENOVATE_TOKEN; +const repoName = process.argv[2]; +const userName = repoName.split('/')[0]; +const packageFile = process.argv[3] || 'package.json'; + +let masterSHA; +let masterPackageJson; + +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'); + }); +}); + +function iterateDependencies(depType) { + const deps = masterPackageJson[depType]; + if (!deps) { + return; } + return Object.keys(deps).reduce((total, depName) => { + return total.then(() => { + const currentVersion = deps[depName].replace(/[^\d.]/g, ''); - updateRepo({ token, repoName, packageFile }) - .catch(err => console.log(err.stack || err)); -} - -function updateRepo({ token, repoName, packageFile }) { - const repoPath = `tmp/${repoName}`; - rimraf.sync(repoPath); - mkdirp.sync(repoPath); - - let repo; - let headCommit; - - return Git - .Clone(`git@github.com:${repoName}.git`, repoPath, { - fetchOpts: { - callbacks: { - credentials: getCredentials, - certificateCheck: () => 1 - } + if (!semver.valid(currentVersion)) { + console.log('Invalid current version'); + return; } - }) - .then(_repo => { - repo = _repo; - return repo.fetch('origin', { - callbacks: { - credentials: getCredentials - } - }); - }) - .then(() => { - return repo.getHeadCommit(); - }) - .then(commit => { - headCommit = commit; - return readFile(headCommit, packageFile); - }) - .then(blob => { - const pkg = JSON.parse(blob); - return iterateDependencies(pkg, 'dependencies') - .then(() => iterateDependencies(pkg, 'devDependencies')); - }) - .then(() => { - rimraf.sync(repoPath); - }); - - function iterateDependencies(pkg, depType) { - const deps = pkg[depType]; - - return Object.keys(deps).reduce((total, depName) => { - return total.then(() => { - const currentVersion = deps[depName].replace(/[^\d.]/g, ''); - if (!semver.valid(currentVersion)) { - return; - } - - // supports scoped packages, e.g. @user/package - return got(`https://registry.npmjs.org/${depName.replace('/', '%2F')}`, { json: true }) - .then(res => { - const latestAvailable = res.body['dist-tags'].latest; - - if (semver.gt(latestAvailable, currentVersion)) { - let majorUpgrade = false; - if (semver.major(latestAvailable) !== semver.major(currentVersion)) { - majorUpgrade = true; + // 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; } - return updateDependency(depType, depName, latestAvailable, majorUpgrade) } }); - }); - }, Promise.resolve()); - } - - function updateDependency(depType, depName, nextVersion, majorUpgrade) { - let branchName = `upgrade/${depName}`; - if (majorUpgrade) { - branchName += '-major'; - } - // try to checkout remote branche - try { - nativeCall(`git checkout ${branchName}`); - } catch (e) { - nativeCall(`git checkout -b ${branchName}`); - } - return updateBranch(branchName, depType, depName, nextVersion, majorUpgrade) - .then(() => nativeCall(`git checkout master`)); - } + let upgradePromises = []; - function updateBranch(branchName, depType, depName, nextVersion, majorUpgrade) { - let commit; + Object.keys(allUpgrades).forEach(function(upgrade) { + const nextVersion = allUpgrades[upgrade]; + upgradePromises.push(updateDependency(depType, depName, currentVersion, nextVersion)); + }); - return repo.getBranchCommit(branchName) - .then(_commit => { - commit = _commit; - return readFile(commit, packageFile); - }) - .then(blob => { - const pkg = JSON.parse(String(blob)); + return Promise.all(upgradePromises); + }); + }); + }, Promise.resolve()); +} - if (pkg[depType][depName] === nextVersion) { - return; +function updateDependency(depType, depName, currentVersion, nextVersion) { + const nextVersionMajor = semver.major(nextVersion); + const branchName = `upgrade/${depName}-${nextVersionMajor}.x`; + let prName = ''; + 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, nextVersion); } - - pkg[depType][depName] = nextVersion; - fs.writeFileSync(`${repoPath}/${packageFile}`, JSON.stringify(pkg, null, 2) + '\n'); - - return commitAndPush(commit, depName, nextVersion, branchName, majorUpgrade); }); + } else { + prName = `Upgrade dependency ${depName} to version ${nextVersion}`; + writeUpdates(depType, depName, branchName, prName, nextVersion); } +} - function commitAndPush(commit, depName, nextVersion, branchName, majorUpgrade) { - let updateMessage = `Update ${depName} to version ${nextVersion}`; - if (majorUpgrade) { - updateMessage += ' (MAJOR)'; +function writeUpdates(depType, depName, branchName, prName, nextVersion) { + const commitMessage = `Upgrade dependency ${depName} to version ${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 => { + if (error.response.body.message !== 'Reference already exists') { + console.log('Error creating branch' + branchName); + console.log(error.response.body); } - console.log(updateMessage); - - let index; - - return repo - .refreshIndex() - .then(indexResult => { - index = indexResult; - return index.addByPath(packageFile); - }) - .then(() => index.write()) - .then(() => index.writeTree()) - .then(oid => { - let author; - - if (authorName && authorEmail) { - const date = new Date(); + }).then(res => { + ghGot(`repos/${repoName}/contents/${packageFile}?ref=${branchName}`, { token: token }) + .then(res => { + const oldFileSHA = res.body.sha; + let branchPackageJson = JSON.parse(new Buffer(res.body.content, 'base64').toString()); + if (branchPackageJson[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'; + + 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); + }); + } + }); + }) + .catch(error => { + console.log('Promise catch'); + }); +} - author = Git.Signature.create( - authorName, - authorEmail, - Math.floor(date.getTime() / 1000), - -date.getTimezoneOffset() - ); - } else { - author = repo.defaultSignature(); +function createOrUpdatePullRequest(branchName, title) { + return ghGot.post(`repos/${repoName}/pulls`, { + token: token, + body: { + title: title, + head: branchName, + base: 'master', + 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; } - - return repo.createCommit('HEAD', author, author, updateMessage, oid, [commit]); - }) - .then(() => Git.Remote.lookup(repo, 'origin')) - .then(origin => { - return origin.push( - [`refs/heads/${branchName}:refs/heads/${branchName}`], { - callbacks: { - credentials: getCredentials - } + const existingPrNo = res.body[0].number; + return ghGot.patch(`repos/${repoName}/pulls/${existingPrNo}`, { + token: token, + body: { + title: title } - ); - }) - .then(() => { - let prTitle = `Update ${depName}`; - if (majorUpgrade) { - prTitle += ' (MAJOR)'; - } - return createPullRequest(branchName, prTitle); + }).then(res => { + console.log('Updated Pull Request: ' + title); + }); }); - } - - function createPullRequest(branchName, updateMessage) { - const head = `${branchName}`; - const options = { - method: 'POST', - json: true, - headers: { - Authorization: `token ${token}` - }, - body: JSON.stringify({ - title: updateMessage, - body: '', - head, - base: 'master' - }) - }; - - return got(`https://api.github.com/repos/${repoName}/pulls`, options) - .then( - null, - err => { - let logError = true; - - try { - if (err.response.body.errors.find(e => e.message.indexOf('A pull request already exists') === 0)) { - logError = false; - } - } catch (e) { - } - - if (logError) { - console.log(err); - } - } - ); - } - - function readFile(commit, filename) { - return commit - .getEntry(packageFile) - .then(entry => entry.getBlob()) - .then(blob => String(blob)); - } - - function getCredentials(url, userName) { - // https://github.com/nodegit/nodegit/issues/1133#issuecomment-261779939 - return Git.Cred.sshKeyNew( - userName, - sshPublicKeyPath, - sshPrivateKeyPath, - '' - ); - } - - function nativeCall(cmd) { - return require('child_process').execSync(cmd, { cwd: repoPath, stdio: [null, null, null] }); - } + } else { + console.log('Error creating Pull Request:'); + console.log(error.response.body); + Promise.reject(); + } + }); } -- GitLab