From 01f399d4fd49c75d1719bfa54512e40913ef662e Mon Sep 17 00:00:00 2001 From: ewnd9 Date: Sun, 18 Dec 2016 07:51:25 +0100 Subject: [PATCH] Initial commit --- package.json | 10 +++ src/index.js | 233 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 package.json create mode 100644 src/index.js diff --git a/package.json b/package.json new file mode 100644 index 0000000000..0981094d09 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "private": true, + "dependencies": { + "got": "^6.6.3", + "mkdirp": "^0.5.1", + "nodegit": "^0.16.0", + "rimraf": "^2.5.4", + "semver": "^5.3.0" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000000..a8108f2ce6 --- /dev/null +++ b/src/index.js @@ -0,0 +1,233 @@ +'use strict'; + +const Git = require('nodegit'); +const got = require('got'); +const semver = require('semver'); +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const rimraf = require('rimraf'); + +const authorName = 'firstName lastName'; // commit credentials +const authorEmail = 'admin@example.com'; // 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]; + + if (!token || !repoName) { + console.error(`Usage: node index.js `); + process.exit(1); + } + + updateRepo({ token, repoName }) + .catch(err => console.log(err.stack || err)); +} + +function updateRepo({ token, repoName }) { + const repoPath = `tmp/${repoName}`; + mkdirp.sync(repoPath); + + let repo; + let headCommit; + + return Git + .Clone(`git@github.com:${repoName}.git`, repoPath, { + fetchOpts: { + callbacks: { + credentials: getCredentials, + certificateCheck: () => 1 + } + } + }) + .then(_repo => { + repo = _repo; + return repo.fetch('origin', { + callbacks: { + credentials: getCredentials + } + }); + }) + .then(() => { + return repo.getHeadCommit(); + }) + .then(commit => { + headCommit = commit; + return readFile(headCommit, 'package.json'); + }) + .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)) { + return updateDependency(depType, depName, latestAvailable) + } + }); + }); + }, Promise.resolve()); + } + + function updateDependency(depType, depName, nextVersion) { + const branchName = `upgrade/${depName}`; + + // try to checkout remote branche + try { + nativeCall(`git checkout ${branchName}`); + } catch (e) { + nativeCall(`git checkout -b ${branchName}`); + } + + return updateBranch(branchName, depType, depName, nextVersion) + .then(() => nativeCall(`git checkout master`)); + } + + function updateBranch(branchName, depType, depName, nextVersion) { + let commit; + + return repo.getBranchCommit(branchName) + .then(_commit => { + commit = _commit; + return readFile(commit, 'package.json'); + }) + .then(blob => { + const pkg = JSON.parse(String(blob)); + + if (pkg[depType][depName] === nextVersion) { + return; + } + + pkg[depType][depName] = nextVersion; + fs.writeFileSync(`${repoPath}/package.json`, JSON.stringify(pkg, null, 2) + '\n'); + + return commitAndPush(commit, depName, nextVersion, branchName); + }); + } + + function commitAndPush(commit, depName, nextVersion, branchName) { + const updateMessage = `Update ${depName} to version ${nextVersion}`; + console.log(updateMessage); + + let index; + + return repo + .refreshIndex() + .then(indexResult => { + index = indexResult; + return index.addByPath('package.json'); + }) + .then(() => index.write()) + .then(() => index.writeTree()) + .then(oid => { + let author; + + if (authorName && authorEmail) { + const date = new Date(); + + author = Git.Signature.create( + authorName, + authorEmail, + Math.floor(date.getTime() / 1000), + -date.getTimezoneOffset() + ); + } else { + author = repo.defaultSignature(); + } + + 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 + } + } + ); + }) + .then(() => { + return createPullRequest(branchName, `Update ${depName}`); + }); + } + + 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('package.json') + .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] }); + } +}