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
This commit is contained in:
Rhys Arkins 2017-01-04 18:48:55 +01:00
Родитель 6161980101
Коммит bcc3171245
2 изменённых файлов: 167 добавлений и 235 удалений

Просмотреть файл

@ -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"
}
}

Просмотреть файл

@ -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 stable = require('semver-stable');
const authorName = 'Renovate Bot'; // commit credentials
const authorEmail = 'renovate-bot@keylocation.sg'; // commit credentials
const token = process.env.RENOVATE_TOKEN;
const repoName = process.argv[2];
const userName = repoName.split('/')[0];
const packageFile = process.argv[3] || 'package.json';
const sshPublicKeyPath = `${process.env.HOME}/.ssh/id_rsa.pub`;
const sshPrivateKeyPath = `${process.env.HOME}/.ssh/id_rsa`;
let masterSHA;
let masterPackageJson;
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';
}
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
}
}
})
.then(_repo => {
repo = _repo;
return repo.fetch('origin', {
callbacks: {
credentials: getCredentials
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;
}
});
})
.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);
// 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(pkg, depType) {
const deps = pkg[depType];
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, '');
if (!semver.valid(currentVersion)) {
console.log('Invalid current version');
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;
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;
}
}
});
if (semver.gt(latestAvailable, currentVersion)) {
let majorUpgrade = false;
if (semver.major(latestAvailable) !== semver.major(currentVersion)) {
majorUpgrade = true;
}
return updateDependency(depType, depName, latestAvailable, majorUpgrade)
}
let upgradePromises = [];
Object.keys(allUpgrades).forEach(function(upgrade) {
const nextVersion = allUpgrades[upgrade];
upgradePromises.push(updateDependency(depType, depName, currentVersion, nextVersion));
});
return Promise.all(upgradePromises);
});
});
}, Promise.resolve());
}
function updateDependency(depType, depName, nextVersion, majorUpgrade) {
let branchName = `upgrade/${depName}`;
if (majorUpgrade) {
branchName += '-major';
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);
}
});
} else {
prName = `Upgrade dependency ${depName} to version ${nextVersion}`;
writeUpdates(depType, depName, branchName, prName, nextVersion);
}
// 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`));
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);
}
}).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';
function updateBranch(branchName, depType, depName, nextVersion, majorUpgrade) {
let commit;
return repo.getBranchCommit(branchName)
.then(_commit => {
commit = _commit;
return readFile(commit, packageFile);
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);
});
}
});
})
.then(blob => {
const pkg = JSON.parse(String(blob));
.catch(error => {
console.log('Promise catch');
});
}
if (pkg[depType][depName] === nextVersion) {
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;
}
pkg[depType][depName] = nextVersion;
fs.writeFileSync(`${repoPath}/${packageFile}`, JSON.stringify(pkg, null, 2) + '\n');
return commitAndPush(commit, depName, nextVersion, branchName, majorUpgrade);
const existingPrNo = res.body[0].number;
return ghGot.patch(`repos/${repoName}/pulls/${existingPrNo}`, {
token: token,
body: {
title: title
}
}).then(res => {
console.log('Updated Pull Request: ' + title);
});
});
}
function commitAndPush(commit, depName, nextVersion, branchName, majorUpgrade) {
let updateMessage = `Update ${depName} to version ${nextVersion}`;
if (majorUpgrade) {
updateMessage += ' (MAJOR)';
}
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();
author = Git.Signature.create(
authorName,
authorEmail,
Math.floor(date.getTime() / 1000),
-date.getTimezoneOffset()
);
} else {
author = repo.defaultSignature();
console.log('Error creating Pull Request:');
console.log(error.response.body);
Promise.reject();
}
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(() => {
let prTitle = `Update ${depName}`;
if (majorUpgrade) {
prTitle += ' (MAJOR)';
}
return createPullRequest(branchName, prTitle);
});
}
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] });
}
}