Skip to content

Commit

Permalink
Gracefully abort if user does not have proper ssh access to github (#61)
Browse files Browse the repository at this point in the history
* Gracefully abort if user does not have ssh access to github

* Update snapshot
  • Loading branch information
sorenlouv authored Apr 12, 2018
1 parent 482b61c commit f913169
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 33 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/node_modules
/coverage
/.coveralls.yml
/test/.DS_Store
5 changes: 3 additions & 2 deletions src/cli/cliService.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,10 @@ function getBranchesByPrompt(branches, isMultipleChoice = false) {
function handleErrors(e) {
switch (e.code) {
// Handled exceptions
case ERROR_CODES.GITHUB_ERROR_CODE:
case ERROR_CODES.MISSING_DATA_ERROR_CODE:
case ERROR_CODES.ABORT_APPLICATION_ERROR_CODE:
case ERROR_CODES.GITHUB_API_ERROR_CODE:
case ERROR_CODES.GITHUB_SSH_ERROR_CODE:
case ERROR_CODES.MISSING_DATA_ERROR_CODE:
logger.error(e.message);
break;

Expand Down
66 changes: 38 additions & 28 deletions src/lib/errors.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,70 @@
const ERROR_CODES = {
INVALID_CONFIG_ERROR_CODE: 'INVALID_CONFIG_ERROR_CODE',
GITHUB_ERROR_CODE: 'GITHUB_ERROR_CODE',
MISSING_DATA_ERROR_CODE: 'MISSING_DATA_ERROR_CODE',
ABORT_APPLICATION_ERROR_CODE: 'ABORT_APPLICATION_ERROR_CODE',
INVALID_JSON_ERROR_CODE: 'INVALID_JSON_ERROR_CODE'
GITHUB_API_ERROR_CODE: 'GITHUB_API_ERROR_CODE',
GITHUB_SSH_ERROR_CODE: 'GITHUB_SSH_ERROR_CODE',
INVALID_CONFIG_ERROR_CODE: 'INVALID_CONFIG_ERROR_CODE',
INVALID_JSON_ERROR_CODE: 'INVALID_JSON_ERROR_CODE',
MISSING_DATA_ERROR_CODE: 'MISSING_DATA_ERROR_CODE'
};

class InvalidConfigError extends Error {
constructor(...args) {
super(...args);
Error.captureStackTrace(this, InvalidConfigError);
this.code = ERROR_CODES.INVALID_CONFIG_ERROR_CODE;
class AbortApplicationError extends Error {
constructor(message) {
super(message);
Error.captureStackTrace(this, AbortApplicationError);
this.code = ERROR_CODES.ABORT_APPLICATION_ERROR_CODE;
this.message = message;
}
}

class GithubError extends Error {
class GithubApiError extends Error {
constructor(message) {
super();
Error.captureStackTrace(this, GithubError);
this.code = ERROR_CODES.GITHUB_ERROR_CODE;
super(message);
Error.captureStackTrace(this, GithubApiError);
this.code = ERROR_CODES.GITHUB_API_ERROR_CODE;
this.message = JSON.stringify(message, null, 4);
}
}

class MissingDataError extends Error {
class GithubSSHError extends Error {
constructor(message) {
super();
Error.captureStackTrace(this, MissingDataError);
this.code = ERROR_CODES.MISSING_DATA_ERROR_CODE;
super(message);
Error.captureStackTrace(this, GithubSSHError);
this.code = ERROR_CODES.GITHUB_SSH_ERROR_CODE;
this.message = message;
}
}

class AbortApplicationError extends Error {
class InvalidConfigError extends Error {
constructor(message) {
super();
Error.captureStackTrace(this, MissingDataError);
this.code = ERROR_CODES.ABORT_APPLICATION_ERROR_CODE;
this.message = message;
super(message);
Error.captureStackTrace(this, InvalidConfigError);
this.code = ERROR_CODES.INVALID_CONFIG_ERROR_CODE;
}
}

class InvalidJsonError extends Error {
constructor(message, filepath, fileContents) {
super(message);
Error.captureStackTrace(this, MissingDataError);
Error.captureStackTrace(this, InvalidJsonError);
this.code = ERROR_CODES.INVALID_JSON_ERROR_CODE;
this.message = `"${filepath}" contains invalid JSON:\n\n${fileContents}\n\nTry validating the file on https://jsonlint.com/`;
}
}

class MissingDataError extends Error {
constructor(message) {
super(message);
Error.captureStackTrace(this, MissingDataError);
this.code = ERROR_CODES.MISSING_DATA_ERROR_CODE;
this.message = message;
}
}

module.exports = {
ERROR_CODES,
InvalidConfigError,
GithubError,
MissingDataError,
AbortApplicationError,
InvalidJsonError
GithubApiError,
GithubSSHError,
InvalidConfigError,
InvalidJsonError,
MissingDataError
};
32 changes: 31 additions & 1 deletion src/lib/git.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const env = require('./env');
const rpc = require('./rpc');
const { GithubSSHError } = require('./errors');

async function folderExists(path) {
try {
Expand All @@ -20,6 +21,7 @@ function repoExists(owner, repoName) {

// Clone repo and add remotes
async function setupRepo(owner, repoName, username) {
await verifyGithubSshAuth();
await rpc.mkdirp(env.getRepoOwnerPath(owner));
await cloneRepo(owner, repoName);
return addRemote(owner, repoName, username);
Expand Down Expand Up @@ -75,7 +77,7 @@ function push(owner, repoName, username, branchName) {
});
}

function resetAndPullMaster(owner, repoName) {
async function resetAndPullMaster(owner, repoName) {
return rpc.exec(
`git reset --hard && git clean -d --force && git checkout master && git pull origin master`,
{
Expand All @@ -84,7 +86,35 @@ function resetAndPullMaster(owner, repoName) {
);
}

async function verifyGithubSshAuth() {
try {
await rpc.exec(`ssh -oBatchMode=yes -T [email protected]`);
return true;
} catch (e) {
switch (e.code) {
case 1:
return true;
case 255:
if (e.stderr.includes('Host key verification failed.')) {
throw new GithubSSHError(
'Host verification of github.com failed. To automatically add it to .ssh/known_hosts run:\nssh -T [email protected]'
);
} else if (e.stderr.includes('Permission denied')) {
throw new GithubSSHError(
'Permission denied. Please add your ssh private key to the keychain by following these steps:\nhttps://help.github.com/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent/#adding-your-ssh-key-to-the-ssh-agent'
);
} else {
throw e;
}

default:
throw e;
}
}
}

module.exports = {
verifyGithubSshAuth,
cherrypick,
cloneRepo,
createAndCheckoutBranch,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/github.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const axios = require('axios');
const querystring = require('querystring');
const get = require('lodash.get');
const { GithubError } = require('./errors');
const { GithubApiError } = require('./errors');

let accessToken;
function getCommitMessage(message) {
Expand Down Expand Up @@ -87,7 +87,7 @@ function setAccessToken(_accessToken) {

function handleError(e) {
if (get(e.response, 'data')) {
throw new GithubError(e.response.data);
throw new GithubApiError(e.response.data);
}

throw e;
Expand Down
3 changes: 3 additions & 0 deletions test/__snapshots__/steps.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

exports[`run through steps exec should be called with correct args 1`] = `
Array [
Array [
"ssh -oBatchMode=yes -T [email protected]",
],
Array [
"git clone [email protected]:elastic/kibana",
Object {
Expand Down
36 changes: 36 additions & 0 deletions test/git.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { verifyGithubSshAuth } = require('../src/lib/git');
const rpc = require('../src/lib/rpc');

describe('verifyGithubSshAuth', () => {
it('github.com is not added to known_hosts file', () => {
const err = new Error();
err.code = 255;
err.stderr = 'Host key verification failed.\r\n';
rpc.exec = jest.fn().mockReturnValue(Promise.reject(err));

return expect(verifyGithubSshAuth()).rejects.toThrow(
'Host verification of github.com failed. To automatically add it to .ssh/known_hosts run:'
);
});

it('ssh key rejected', async () => {
const err = new Error();
err.code = 255;
err.stderr = '[email protected]: Permission denied (publickey).\r\n';
rpc.exec = jest.fn().mockReturnValue(Promise.reject(err));

return expect(verifyGithubSshAuth()).rejects.toThrowError(
'Permission denied. Please add your ssh private key to the keychain by following these steps:'
);
});

it('user is successfully authenticated', async () => {
const err = new Error();
err.code = 1;
err.stderr =
"Hi sqren! You've successfully authenticated, but GitHub does not provide shell access.\n";
rpc.exec = jest.fn().mockReturnValue(Promise.reject(err));

return expect(verifyGithubSshAuth()).resolves.toBe(true);
});
});

0 comments on commit f913169

Please sign in to comment.