Skip to content

Commit

Permalink
feat(update): add auto rollback of failed updates
Browse files Browse the repository at this point in the history
refs #759
- ask user if we wants to bring back previous version if
  - GhostError was thrown (migration failed)
  - if the user has not executed `ghost update --rollback`
- show error to user so he knows why the update failed
  • Loading branch information
vikaspotluri123 authored and acburdine committed Aug 16, 2018
1 parent 702e953 commit 4f00353
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 1 deletion.
40 changes: 39 additions & 1 deletion lib/commands/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});
});
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
192 changes: 192 additions & 0 deletions test/unit/commands/update-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 4f00353

Please sign in to comment.