Skip to content

Commit 71d49f6

Browse files
feat(update): add support for automatically rolling back failed updates
1 parent 0b4b606 commit 71d49f6

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed

lib/commands/update.js

+26-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ class UpdateCommand extends Command {
114114
return;
115115
}
116116

117-
return this.ui.listr(tasks, context);
117+
return this.ui.listr(tasks, context).catch(error =>
118+
this.rollbackFromFail(error, context.version, argv['auto-rollback'])
119+
);
118120
});
119121
});
120122
}
@@ -196,6 +198,24 @@ class UpdateCommand extends Command {
196198
});
197199
}
198200

201+
rollbackFromFail(error, newVer, force = false) {
202+
const oldVer = this.system.getInstance().cliConfig.get('previous-version');
203+
const question = `Unable to upgrade Ghost from v${oldVer} to v${newVer}. Would you like to revert back to v${oldVer}?`;
204+
const promise = force ? Promise.resolve(true) : this.ui.confirm(question, true);
205+
206+
this.ui.error(error);
207+
208+
return promise.then(answer => {
209+
if (!answer) {
210+
return Promise.resolve();
211+
}
212+
213+
return this.run({
214+
rollback: true
215+
});
216+
});
217+
}
218+
199219
link(context) {
200220
const symlinkSync = require('symlink-or-copy').sync;
201221

@@ -233,6 +253,11 @@ UpdateCommand.options = {
233253
describe: 'Limit update to Ghost 1.x releases',
234254
type: 'boolean',
235255
default: false
256+
},
257+
'auto-rollback': {
258+
description: '[--no-auto-rollback] Enable/Disable automatically rolling back Ghost if updating fails',
259+
type: 'boolean',
260+
default: false
236261
}
237262
};
238263
UpdateCommand.checkVersion = true;

test/unit/commands/update-spec.js

+95
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,44 @@ describe('Unit: Commands > Update', function () {
480480
expect(stopStub.called).to.be.false;
481481
});
482482
});
483+
484+
it('attempts to auto-rollback on error', function () {
485+
const UpdateCommand = require(modulePath);
486+
const config = configStub();
487+
const errObj = new Error('should_rollback')
488+
config.get.withArgs('cli-version').returns('1.0.0');
489+
config.get.withArgs('active-version').returns('1.1.0');
490+
config.get.withArgs('previous-version').returns('1.0.0');
491+
492+
const ui = {
493+
log: sinon.stub(),
494+
listr: sinon.stub().rejects(errObj),
495+
run: sinon.stub().callsFake(fn => fn())
496+
};
497+
const system = {getInstance: sinon.stub()};
498+
class TestInstance extends Instance {
499+
get cliConfig() { return config; }
500+
}
501+
const fakeInstance = sinon.stub(new TestInstance(ui, system, '/var/www/ghost'));
502+
system.getInstance.returns(fakeInstance);
503+
fakeInstance.running.resolves(false);
504+
505+
const cmdInstance = new UpdateCommand(ui, system);
506+
const rollback = cmdInstance.rollbackFromFail = sinon.stub().rejects(new Error('rollback_successful'));
507+
cmdInstance.runCommand = sinon.stub().resolves(true);
508+
cmdInstance.version = sinon.stub().callsFake(context => {
509+
context.version = '1.1.1';
510+
return true;
511+
});
512+
513+
return cmdInstance.run({}).then(() => {
514+
expect(false, 'Promise should have rejected').to.be.true;
515+
}).catch(error => {
516+
expect(error.message).to.equal('rollback_successful');
517+
expect(rollback.calledOnce).to.be.true;
518+
expect(rollback.calledWithExactly(errObj, '1.1.1', undefined)).to.be.true;
519+
});
520+
});
483521
});
484522

485523
describe('downloadAndUpdate task', function () {
@@ -796,6 +834,63 @@ describe('Unit: Commands > Update', function () {
796834
});
797835
});
798836

837+
describe('rollbackFromFail', function () {
838+
let ui, system;
839+
840+
beforeEach(function () {
841+
const cliConfig = {get: () => '1.0.0'}
842+
ui = {
843+
confirm: sinon.stub(),
844+
error: () => true
845+
};
846+
847+
system = {
848+
getInstance() {
849+
return {cliConfig};
850+
}
851+
};
852+
});
853+
854+
it('Asks to rollback by default', function () {
855+
const UpdateCommand = require(modulePath);
856+
const expectedQuestion = 'Unable to upgrade Ghost from v1.0.0 to v1.1.1. Would you like to revert back to v1.0.0?'
857+
const update = new UpdateCommand(ui, system, '/var/www/ghost');
858+
ui.confirm.rejects(new Error('CONFIRMED'));
859+
860+
return update.rollbackFromFail(false, '1.1.1').then(() => {
861+
expect(false, 'Promise should have rejected').to.be.true;
862+
}).catch(error => {
863+
expect(error.message).to.equal('CONFIRMED');
864+
expect(ui.confirm.calledOnce).to.be.true;
865+
expect(ui.confirm.calledWithExactly(expectedQuestion, true)).to.be.true;
866+
});
867+
});
868+
869+
it('Listens to the user', function () {
870+
const UpdateCommand = require(modulePath);
871+
const update = new UpdateCommand(ui, system, '/var/www/ghost');
872+
873+
ui.confirm.resolves(false);
874+
update.run = sinon.stub().resolves();
875+
876+
return update.rollbackFromFail(false, '1.1.1').then(() => {
877+
expect(ui.confirm.calledOnce).to.be.true;
878+
expect(update.run.called).to.be.false;
879+
});
880+
});
881+
882+
it('Re-runs `run` using rollback', function () {
883+
const UpdateCommand = require(modulePath);
884+
const update = new UpdateCommand(ui, system, '/var/www/ghost');
885+
886+
update.run = sinon.stub().resolves();
887+
return update.rollbackFromFail(false, '1.1.1', true).then(() => {
888+
expect(update.run.calledOnce).to.be.true;
889+
expect(update.run.calledWithExactly({rollback: true})).to.be.true;
890+
});
891+
});
892+
});
893+
799894
describe('link', function () {
800895
const UpdateCommand = require(modulePath);
801896

0 commit comments

Comments
 (0)