diff --git a/lib/commands/update.js b/lib/commands/update.js index 51942df2e..be9750f5b 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -114,7 +114,14 @@ class UpdateCommand extends Command { return; } - return this.ui.listr(tasks, context); + return this.ui.listr(tasks, context) + .catch((error) => { + if (error instanceof errors.GhostError && !context.rollback) { + return this.rollbackFromFail(error, context.version, argv['auto-rollback']); + } + + throw error; + }); }); }); } @@ -195,6 +202,32 @@ class UpdateCommand extends Command { }); } + rollbackFromFail(error, newVer, force = false) { + const oldVer = this.system.getInstance().cliConfig.get('previous-version'); + const question = `Unable to upgrade Ghost from v${oldVer} to v${newVer}. Would you like to revert back to v${oldVer}?`; + + this.ui.error(error, this.system); + this.ui.log('\n\n'); + + if (force) { + return this.run({ + restart: true, + rollback: true + }); + } + + return this.ui.confirm(question, true).then(answer => { + if (!answer) { + return Promise.resolve(); + } + + return this.run({ + restart: true, + rollback: true + }); + }); + } + link(context) { const symlinkSync = require('symlink-or-copy').sync; @@ -232,6 +265,11 @@ UpdateCommand.options = { describe: 'Limit update to Ghost 1.x releases', type: 'boolean', default: false + }, + 'auto-rollback': { + description: '[--no-auto-rollback] Enable/Disable automatically rolling back Ghost if updating fails', + type: 'boolean', + default: false } }; UpdateCommand.checkVersion = true; diff --git a/test/unit/commands/update-spec.js b/test/unit/commands/update-spec.js index 799f7720b..27a6e172a 100644 --- a/test/unit/commands/update-spec.js +++ b/test/unit/commands/update-spec.js @@ -480,6 +480,118 @@ describe('Unit: Commands > Update', function () { expect(stopStub.called).to.be.false; }); }); + + it('attempts to auto-rollback on ghost error', function () { + const UpdateCommand = require(modulePath); + const config = configStub(); + const errObj = new errors.GhostError('should_rollback'); + config.get.withArgs('cli-version').returns('1.0.0'); + config.get.withArgs('active-version').returns('1.1.0'); + config.get.withArgs('previous-version').returns('1.0.0'); + + const ui = { + log: sinon.stub(), + listr: sinon.stub().rejects(errObj), + run: sinon.stub().callsFake(fn => fn()) + }; + const system = {getInstance: sinon.stub()}; + class TestInstance extends Instance { + get cliConfig() { return config; } + } + const fakeInstance = sinon.stub(new TestInstance(ui, system, '/var/www/ghost')); + system.getInstance.returns(fakeInstance); + fakeInstance.running.resolves(false); + + const cmdInstance = new UpdateCommand(ui, system); + const rollback = cmdInstance.rollbackFromFail = sinon.stub().rejects(new Error('rollback_successful')); + cmdInstance.runCommand = sinon.stub().resolves(true); + cmdInstance.version = sinon.stub().callsFake(context => { + context.version = '1.1.1'; + return true; + }); + + return cmdInstance.run({}).then(() => { + expect(false, 'Promise should have rejected').to.be.true; + }).catch(error => { + expect(error.message).to.equal('rollback_successful'); + expect(rollback.calledOnce).to.be.true; + expect(rollback.calledWithExactly(errObj, '1.1.1', undefined)).to.be.true; + }); + }); + + it('does not attempts to auto-rollback on cli error', function () { + const UpdateCommand = require(modulePath); + const config = configStub(); + const errObj = new Error('do_nothing'); + config.get.withArgs('cli-version').returns('1.0.0'); + config.get.withArgs('active-version').returns('1.1.0'); + config.get.withArgs('previous-version').returns('1.0.0'); + + const ui = { + log: sinon.stub(), + listr: sinon.stub().rejects(errObj), + run: sinon.stub().callsFake(fn => fn()) + }; + const system = {getInstance: sinon.stub()}; + class TestInstance extends Instance { + get cliConfig() { return config; } + } + const fakeInstance = sinon.stub(new TestInstance(ui, system, '/var/www/ghost')); + system.getInstance.returns(fakeInstance); + fakeInstance.running.resolves(false); + + const cmdInstance = new UpdateCommand(ui, system); + const rollback = cmdInstance.rollbackFromFail = sinon.stub(); + cmdInstance.runCommand = sinon.stub().resolves(true); + cmdInstance.version = sinon.stub().callsFake(context => { + context.version = '1.1.1'; + return true; + }); + + return cmdInstance.run({}).then(() => { + expect(false, 'Promise should have rejected').to.be.true; + }).catch(error => { + expect(error.message).to.equal('do_nothing'); + expect(rollback.called).to.be.false; + }); + }); + + it('does not attempts to auto-rollback on ghost error if rollback is used', function () { + const UpdateCommand = require(modulePath); + const config = configStub(); + const errObj = new errors.GhostError('do_nothing'); + config.get.withArgs('cli-version').returns('1.0.0'); + config.get.withArgs('active-version').returns('1.1.0'); + config.get.withArgs('previous-version').returns('1.0.0'); + + const ui = { + log: sinon.stub(), + listr: sinon.stub().rejects(errObj), + run: sinon.stub().callsFake(fn => fn()) + }; + const system = {getInstance: sinon.stub()}; + class TestInstance extends Instance { + get cliConfig() { return config; } + } + const fakeInstance = sinon.stub(new TestInstance(ui, system, '/var/www/ghost')); + system.getInstance.returns(fakeInstance); + fakeInstance.running.resolves(false); + + const cmdInstance = new UpdateCommand(ui, system); + const rollback = cmdInstance.rollbackFromFail = sinon.stub(); + cmdInstance.runCommand = sinon.stub().resolves(true); + cmdInstance.version = sinon.stub().callsFake(context => { + context.version = '1.1.1'; + return true; + }); + + return cmdInstance.run({rollback: true}).then(() => { + expect(false, 'Promise should have rejected').to.be.true; + }).catch(error => { + expect(error.message).to.equal('do_nothing'); + expect(rollback.called).to.be.false; + }); + }); }); describe('downloadAndUpdate task', function () { @@ -796,6 +908,86 @@ describe('Unit: Commands > Update', function () { }); }); + describe('rollbackFromFail', function () { + let ui, system; + + beforeEach(function () { + const cliConfig = {get: () => '1.0.0'} + ui = { + log: sinon.stub(), + confirm: sinon.stub(), + error: sinon.stub() + }; + + system = { + getInstance() { + return {cliConfig}; + } + }; + }); + + it('Asks to rollback by default', function () { + const UpdateCommand = require(modulePath); + const expectedQuestion = 'Unable to upgrade Ghost from v1.0.0 to v1.1.1. Would you like to revert back to v1.0.0?' + const update = new UpdateCommand(ui, system, '/var/www/ghost'); + ui.confirm.resolves(true); + update.run = sinon.stub().resolves(); + + return update.rollbackFromFail(false, '1.1.1').then(() => { + expect(ui.log.calledOnce).to.be.true; + expect(ui.error.calledOnce).to.be.true; + + expect(ui.confirm.calledOnce).to.be.true; + expect(ui.confirm.calledWithExactly(expectedQuestion, true)).to.be.true; + expect(update.run.calledOnce).to.be.true; + }); + }); + + it('Listens to the user', function () { + const UpdateCommand = require(modulePath); + const update = new UpdateCommand(ui, system, '/var/www/ghost'); + + ui.confirm.resolves(false); + update.run = sinon.stub().resolves(); + + return update.rollbackFromFail(false, '1.1.1').then(() => { + expect(ui.log.calledOnce).to.be.true; + expect(ui.error.calledOnce).to.be.true; + + expect(ui.confirm.calledOnce).to.be.true; + expect(update.run.called).to.be.false; + }); + }); + + it('Force update', function () { + const UpdateCommand = require(modulePath); + const update = new UpdateCommand(ui, system, '/var/www/ghost'); + update.run = sinon.stub().resolves(); + + return update.rollbackFromFail(new Error('test'), '1.1.1', true).then(() => { + expect(ui.log.calledOnce).to.be.true; + expect(ui.error.calledOnce).to.be.true; + + expect(ui.confirm.called).to.be.false; + expect(update.run.calledOnce).to.be.true; + }); + }); + + it('Re-runs `run` using rollback', function () { + const UpdateCommand = require(modulePath); + const update = new UpdateCommand(ui, system, '/var/www/ghost'); + + update.run = sinon.stub().resolves(); + return update.rollbackFromFail(false, '1.1.1', true).then(() => { + expect(ui.log.calledOnce).to.be.true; + expect(ui.error.calledOnce).to.be.true; + + expect(update.run.calledOnce).to.be.true; + expect(update.run.calledWithExactly({rollback: true, restart: true})).to.be.true; + }); + }); + }); + describe('link', function () { const UpdateCommand = require(modulePath);