Skip to content

Commit cff1ed2

Browse files
committed
feat(install): add option to install/update using zip file
closes #59 - add a --zip option to both `ghost install` and `ghost update` so that unreleased new Ghost versions can be tested using install and update - deps: [email protected] - update tests
1 parent 46e454d commit cff1ed2

14 files changed

+275
-40
lines changed

lib/commands/install.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class InstallCommand extends Command {
5959
title: 'Downloading and installing Ghost',
6060
task: (ctx, task) => {
6161
task.title = `Downloading and installing Ghost v${ctx.version}`;
62-
return yarnInstall(ctx.ui);
62+
return yarnInstall(ctx.ui, ctx.zip);
6363
}
6464
}, {
6565
title: 'Finishing install process',
@@ -71,6 +71,7 @@ class InstallCommand extends Command {
7171
task: this.casper
7272
}], false)
7373
}], {
74+
zip: argv.zip,
7475
version: version,
7576
cliVersion: this.system.cliVersion
7677
})
@@ -86,9 +87,11 @@ class InstallCommand extends Command {
8687
}
8788

8889
version(ctx) {
89-
const resolveVersion = require('../utils/resolve-version');
90+
const versionResolver = ctx.zip ?
91+
require('../utils/version-from-zip') :
92+
require('../utils/resolve-version');
9093

91-
return resolveVersion(ctx.version).then((version) => {
94+
return versionResolver(ctx.zip || ctx.version).then((version) => {
9295
ctx.version = version;
9396
ctx.installPath = path.join(process.cwd(), 'versions', version);
9497
});
@@ -124,6 +127,10 @@ InstallCommand.options = {
124127
description: 'Folder to install Ghost in',
125128
type: 'string'
126129
},
130+
zip: {
131+
description: 'Path to Ghost release zip to install',
132+
type: 'string'
133+
},
127134
stack: {
128135
description: '[--no-stack] Enable/Disable system stack checks on install',
129136
type: 'boolean',

lib/commands/update.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ class UpdateCommand extends Command {
3030
instance: instance,
3131
force: argv.force,
3232
activeVersion: instance.cliConfig.get('active-version'),
33-
version: argv.version
33+
version: argv.version,
34+
zip: argv.zip
3435
};
3536

3637
if (argv.rollback) {
@@ -97,7 +98,7 @@ class UpdateCommand extends Command {
9798
}
9899

99100
task.title = `Downloading and updating Ghost to v${ctx.version}`;
100-
return yarnInstall(ctx.ui);
101+
return yarnInstall(ctx.ui, ctx.zip);
101102
}
102103

103104
stop() {
@@ -137,13 +138,18 @@ class UpdateCommand extends Command {
137138
}
138139

139140
version(context) {
140-
const resolveVersion = require('../utils/resolve-version');
141-
142141
if (context.rollback) {
143142
return Promise.resolve(true);
144143
}
145144

146-
return resolveVersion(context.version, context.force ? null : context.activeVersion).then((version) => {
145+
const versionResolver = context.zip ?
146+
require('../utils/version-from-zip') :
147+
require('../utils/resolve-version');
148+
149+
return versionResolver(
150+
context.zip || context.version,
151+
context.force ? null : context.activeVersion
152+
).then((version) => {
147153
context.version = version;
148154
context.installPath = path.join(process.cwd(), 'versions', version);
149155
return true;
@@ -170,6 +176,10 @@ class UpdateCommand extends Command {
170176
UpdateCommand.description = 'Update a Ghost instance';
171177
UpdateCommand.params = '[version]';
172178
UpdateCommand.options = {
179+
zip: {
180+
description: 'Path to Ghost release zip to install',
181+
type: 'string'
182+
},
173183
rollback: {
174184
alias: 'r',
175185
description: 'Rollback to the previously installed Ghost version',

lib/tasks/yarn-install.js

+11-4
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,27 @@ const subTasks = {
4444
}
4545
};
4646

47-
module.exports = function yarnInstall(ui) {
48-
return ui.listr([{
47+
module.exports = function yarnInstall(ui, zipFile) {
48+
const tasks = zipFile ? [{
49+
title: 'Extracting release from local zip',
50+
task: (ctx) => decompress(zipFile, ctx.installPath)
51+
}] : [{
4952
title: 'Getting download information',
5053
task: subTasks.dist
5154
}, {
5255
title: 'Downloading',
5356
task: subTasks.download
54-
}, {
57+
}];
58+
59+
tasks.push({
5560
title: 'Installing dependencies',
5661
task: (ctx) => yarn(['install', '--no-emoji', '--no-progress'], {
5762
cwd: ctx.installPath,
5863
env: {NODE_ENV: 'production'},
5964
observe: true
6065
})
61-
}], false);
66+
});
67+
68+
return ui.listr(tasks, false);
6269
};
6370
module.exports.subTasks = subTasks;

lib/utils/version-from-zip.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
const fs = require('fs');
3+
const path = require('path');
4+
const errors = require('../errors');
5+
const semver = require('semver');
6+
const AdmZip = require('adm-zip');
7+
8+
module.exports = function versionFromZip(zipPath, currentVersion) {
9+
if (!path.isAbsolute(zipPath)) {
10+
zipPath = path.join(process.cwd(), zipPath);
11+
}
12+
13+
if (!fs.existsSync(zipPath) || path.extname(zipPath) !== '.zip') {
14+
return Promise.reject(new errors.SystemError('Zip file could not be found.'));
15+
}
16+
17+
const zip = new AdmZip(zipPath);
18+
let pkg;
19+
20+
try {
21+
pkg = JSON.parse(zip.readAsText('package.json'));
22+
} catch (e) {
23+
return Promise.reject(new errors.SystemError('Zip file does not contain a valid package.json.'));
24+
}
25+
26+
if (pkg.name !== 'ghost') {
27+
return Promise.reject(new errors.SystemError('Zip file does not contain a Ghost release.'));
28+
}
29+
30+
if (semver.lt(pkg.version, '1.0.0')) {
31+
return Promise.reject(new errors.SystemError('Zip file contains pre-1.0 version of Ghost.'));
32+
}
33+
34+
if (currentVersion && semver.lt(pkg.version, currentVersion)) {
35+
return Promise.reject(
36+
new errors.SystemError('Zip file contains an older release version than what is currently installed.')
37+
);
38+
}
39+
40+
return Promise.resolve(pkg.version);
41+
};

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
},
3939
"preferGlobal": true,
4040
"dependencies": {
41-
"abbrev": "^1.1.0",
41+
"abbrev": "1.1.0",
42+
"adm-zip": "0.4.7",
4243
"bluebird": "3.5.0",
4344
"chalk": "2.0.1",
4445
"cli-table2": "0.2.0",

test/fixtures/ghostlts.zip

216 Bytes
Binary file not shown.

test/fixtures/ghostold.zip

215 Bytes
Binary file not shown.

test/fixtures/ghostrelease.zip

215 Bytes
Binary file not shown.

test/fixtures/nopkg.zip

206 Bytes
Binary file not shown.

test/fixtures/notghost.zip

217 Bytes
Binary file not shown.

test/unit/commands/install-spec.js

+56-18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const expect = require('chai').expect;
33
const sinon = require('sinon');
44
const proxyquire = require('proxyquire').noCallThru();
5+
const Promise = require('bluebird');
56
const path = require('path');
67

78
const modulePath = '../../../lib/commands/install';
@@ -116,7 +117,7 @@ describe('Unit: Commands > Install', function () {
116117
});
117118
let testInstance = new InstallCommand({listr: listrStub}, {cliVersion: '1.0.0', setEnvironment: setEnvironmentStub});
118119

119-
return testInstance.run({version: 'local'}).then(() => {
120+
return testInstance.run({version: 'local', zip: ''}).then(() => {
120121
expect(false, 'run should have rejected').to.be.true;
121122
}).catch(() => {
122123
expect(readdirStub.calledOnce).to.be.true;
@@ -127,12 +128,13 @@ describe('Unit: Commands > Install', function () {
127128
]);
128129
expect(listrStub.args[0][1]).to.deep.equal({
129130
ui: {listr: listrStub},
130-
argv: {version: 'local'},
131+
argv: {version: 'local', zip: ''},
131132
local: true
132133
});
133134
expect(listrStub.args[1][1]).to.deep.equal({
134135
version: null,
135-
cliVersion: '1.0.0'
136+
cliVersion: '1.0.0',
137+
zip: ''
136138
});
137139
expect(setEnvironmentStub.calledOnce).to.be.true;
138140
expect(setEnvironmentStub.calledWithExactly(true, true)).to.be.true;
@@ -152,7 +154,7 @@ describe('Unit: Commands > Install', function () {
152154
});
153155
let testInstance = new InstallCommand({listr: listrStub}, {cliVersion: '1.0.0', setEnvironment: setEnvironmentStub});
154156

155-
return testInstance.run({version: '1.5.0', local: true}).then(() => {
157+
return testInstance.run({version: '1.5.0', local: true, zip: ''}).then(() => {
156158
expect(false, 'run should have rejected').to.be.true;
157159
}).catch(() => {
158160
expect(readdirStub.calledOnce).to.be.true;
@@ -163,31 +165,47 @@ describe('Unit: Commands > Install', function () {
163165
]);
164166
expect(listrStub.args[0][1]).to.deep.equal({
165167
ui: {listr: listrStub},
166-
argv: {version: '1.5.0', local: true},
168+
argv: {version: '1.5.0', local: true, zip: ''},
167169
local: true
168170
});
169171
expect(listrStub.args[1][1]).to.deep.equal({
170172
version: '1.5.0',
171-
cliVersion: '1.0.0'
173+
cliVersion: '1.0.0',
174+
zip: ''
172175
});
173176
expect(setEnvironmentStub.calledOnce).to.be.true;
174177
expect(setEnvironmentStub.calledWithExactly(true, true)).to.be.true;
175178
});
176179
});
177180

178-
it('returns after tasks run if --no-setup is passed', function () {
179-
let readdirStub = sandbox.stub().returns([]);
180-
let listrStub = sandbox.stub().resolves();
181+
it('calls all tasks and returns after tasks run if --no-setup is passed', function () {
182+
const readdirStub = sandbox.stub().returns([]);
183+
const yarnInstallStub = sandbox.stub().resolves();
184+
const ensureStructureStub = sandbox.stub().resolves();
185+
const listrStub = sandbox.stub().callsFake((tasks, ctx) => {
186+
return Promise.each(tasks, task => task.task(ctx, {}));
187+
});
181188

182189
const InstallCommand = proxyquire(modulePath, {
183-
'fs-extra': {readdirSync: readdirStub}
190+
'fs-extra': {readdirSync: readdirStub},
191+
'../tasks/yarn-install': yarnInstallStub,
192+
'../tasks/ensure-structure': ensureStructureStub,
193+
'./doctor/checks/install': []
184194
});
185-
let testInstance = new InstallCommand({listr: listrStub}, {cliVersion: '1.0.0'});
186-
let runCommandStub = sandbox.stub(testInstance, 'runCommand').resolves();
195+
const testInstance = new InstallCommand({listr: listrStub}, {cliVersion: '1.0.0'});
196+
const runCommandStub = sandbox.stub(testInstance, 'runCommand').resolves();
197+
const versionStub = sandbox.stub(testInstance, 'version').resolves();
198+
const linkStub = sinon.stub(testInstance, 'link').resolves();
199+
const casperStub = sinon.stub(testInstance, 'casper').resolves();
187200

188201
return testInstance.run({version: '1.0.0', setup: false}).then(() => {
189202
expect(readdirStub.calledOnce).to.be.true;
190-
expect(listrStub.calledTwice).to.be.true;
203+
expect(listrStub.calledThrice).to.be.true;
204+
expect(yarnInstallStub.calledOnce).to.be.true;
205+
expect(ensureStructureStub.calledOnce).to.be.true;
206+
expect(versionStub.calledOnce).to.be.true;
207+
expect(linkStub.calledOnce).to.be.true;
208+
expect(casperStub.calledOnce).to.be.true;
191209
expect(runCommandStub.called).to.be.false;
192210
});
193211
});
@@ -204,36 +222,56 @@ describe('Unit: Commands > Install', function () {
204222
let testInstance = new InstallCommand({listr: listrStub}, {cliVersion: '1.0.0', setEnvironment: setEnvironmentStub});
205223
let runCommandStub = sandbox.stub(testInstance, 'runCommand').resolves();
206224

207-
return testInstance.run({version: 'local', setup: true}).then(() => {
225+
return testInstance.run({version: 'local', setup: true, zip: ''}).then(() => {
208226
expect(readdirStub.calledOnce).to.be.true;
209227
expect(listrStub.calledTwice).to.be.true;
210228
expect(setEnvironmentStub.calledOnce).to.be.true;
211229
expect(setEnvironmentStub.calledWithExactly(true, true));
212230
expect(runCommandStub.calledOnce).to.be.true;
213231
expect(runCommandStub.calledWithExactly(
214232
{SetupCommand: true},
215-
{version: 'local', local: true}
233+
{version: 'local', local: true, zip: ''}
216234
));
217235
});
218236
});
219237
});
220238

221239
describe('tasks > version', function () {
222240
it('calls resolveVersion, sets version and install path', function () {
223-
let resolveVersionStub = sinon.stub().resolves('1.5.0');
241+
const resolveVersionStub = sinon.stub().resolves('1.5.0');
224242
const InstallCommand = proxyquire(modulePath, {
225243
'../utils/resolve-version': resolveVersionStub
226244
});
227245

228-
let testInstance = new InstallCommand({}, {});
229-
let context = {version: '1.0.0'};
246+
const testInstance = new InstallCommand({}, {});
247+
const context = {version: '1.0.0'};
230248

231249
return testInstance.version(context).then(() => {
232250
expect(resolveVersionStub.calledOnce).to.be.true;
233251
expect(context.version).to.equal('1.5.0');
234252
expect(context.installPath).to.equal(path.join(process.cwd(), 'versions/1.5.0'));
235253
});
236254
});
255+
256+
it('calls versionFromZip if zip file is passed in context', function () {
257+
const resolveVersionStub = sinon.stub().resolves('1.5.0');
258+
const zipVersionStub = sinon.stub().resolves('1.5.2');
259+
const InstallCommand = proxyquire(modulePath, {
260+
'../utils/resolve-version': resolveVersionStub,
261+
'../utils/version-from-zip': zipVersionStub
262+
});
263+
264+
const testInstance = new InstallCommand({}, {});
265+
const context = {version: '1.0.0', zip: '/some/zip/file.zip'};
266+
267+
return testInstance.version(context).then(() => {
268+
expect(resolveVersionStub.called).to.be.false;
269+
expect(zipVersionStub.calledOnce).to.be.true;
270+
expect(zipVersionStub.calledWith('/some/zip/file.zip')).to.be.true;
271+
expect(context.version).to.equal('1.5.2');
272+
expect(context.installPath).to.equal(path.join(process.cwd(), 'versions/1.5.2'));
273+
});
274+
});
237275
});
238276

239277
describe('tasks > casper', function () {

0 commit comments

Comments
 (0)