From cc1ebc8bbef8520f453fdfa8141b6da555567019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 11 Apr 2018 21:32:43 +0200 Subject: [PATCH 1/2] Gracefully abort if user does not have ssh access to github --- src/cli/cliService.js | 5 ++-- src/lib/errors.js | 66 +++++++++++++++++++++++++------------------ src/lib/git.js | 32 ++++++++++++++++++++- src/lib/github.js | 4 +-- test/git.test.js | 36 +++++++++++++++++++++++ 5 files changed, 110 insertions(+), 33 deletions(-) create mode 100644 test/git.test.js diff --git a/src/cli/cliService.js b/src/cli/cliService.js index 431050d0..17b1e6ac 100644 --- a/src/cli/cliService.js +++ b/src/cli/cliService.js @@ -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; diff --git a/src/lib/errors.js b/src/lib/errors.js index 7bc832f0..c74dfc54 100644 --- a/src/lib/errors.js +++ b/src/lib/errors.js @@ -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 }; diff --git a/src/lib/git.js b/src/lib/git.js index e28974f4..6d2e3eac 100644 --- a/src/lib/git.js +++ b/src/lib/git.js @@ -1,5 +1,6 @@ const env = require('./env'); const rpc = require('./rpc'); +const { GithubSSHError } = require('./errors'); async function folderExists(path) { try { @@ -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); @@ -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`, { @@ -84,7 +86,35 @@ function resetAndPullMaster(owner, repoName) { ); } +async function verifyGithubSshAuth() { + try { + await rpc.exec(`ssh -oBatchMode=yes -T git@github.com`); + 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 git@github.com' + ); + } 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, diff --git a/src/lib/github.js b/src/lib/github.js index 6f2a968f..c76c7e5f 100644 --- a/src/lib/github.js +++ b/src/lib/github.js @@ -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) { @@ -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; diff --git a/test/git.test.js b/test/git.test.js new file mode 100644 index 00000000..b2543eac --- /dev/null +++ b/test/git.test.js @@ -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 = 'git@github.com: 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); + }); +}); From d5a4bf853a9244ff51cc4ffa809031b479d5c091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 11 Apr 2018 22:00:49 +0200 Subject: [PATCH 2/2] Update snapshot --- .gitignore | 1 + test/__snapshots__/steps.test.js.snap | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index fd4b7cec..bd5e40d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules /coverage /.coveralls.yml +/test/.DS_Store diff --git a/test/__snapshots__/steps.test.js.snap b/test/__snapshots__/steps.test.js.snap index 64397ba0..d3937f42 100644 --- a/test/__snapshots__/steps.test.js.snap +++ b/test/__snapshots__/steps.test.js.snap @@ -2,6 +2,9 @@ exports[`run through steps exec should be called with correct args 1`] = ` Array [ + Array [ + "ssh -oBatchMode=yes -T git@github.com", + ], Array [ "git clone git@github.com:elastic/kibana", Object {