Skip to content

Commit

Permalink
feat(archive): added support for installing/updating from tarballs
Browse files Browse the repository at this point in the history
- we currently support installing/updating via a zip
- Ghost zips are slowly becoming deprecated as we move towards a single
  packaging method via `npm pack`
- this means Ghost-CLI needs support to install a tar.gz or .tgz file
- this commit adds such support
- I've added support for `--archive` and maintained `--zip`, but it's
  just an alias between the two
- also has the nice side-effect of switching from `adm-zip` to
  `decompress`, so this commit also cleans up a dependency
  • Loading branch information
daniellockyer committed Aug 10, 2022
1 parent 744db0b commit a079cdc
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 76 deletions.
9 changes: 5 additions & 4 deletions lib/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class InstallCommand extends Command {
async version(ctx) {
const semver = require('semver');
const {SystemError} = require('../errors');
const {resolveVersion, versionFromZip} = require('../utils/version');
const {resolveVersion, versionFromArchive} = require('../utils/version');
let {version, zip, v1, fromExport, force, channel} = ctx.argv;
let exportVersion = null;

Expand All @@ -111,12 +111,12 @@ class InstallCommand extends Command {
}

if (version && zip) {
ctx.ui.log('Warning: you specified both a specific version and a zip file. The version in the zip file will be used.', 'yellow');
ctx.ui.log('Warning: you specified both a specific version and an archive file. The version in the archive will be used.', 'yellow');
}

let resolvedVersion = null;
if (zip) {
resolvedVersion = await versionFromZip(zip);
resolvedVersion = await versionFromArchive(zip);
} else {
resolvedVersion = await resolveVersion(version, null, {v1, force, channel});
}
Expand Down Expand Up @@ -165,7 +165,8 @@ InstallCommand.options = {
type: 'string'
},
zip: {
description: 'Path to Ghost release zip to install',
alias: 'archive',
description: 'Path to Ghost release archive to install',
type: 'string'
},
version1: {
Expand Down
7 changes: 4 additions & 3 deletions lib/commands/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ class UpdateCommand extends Command {
}

async version(context) {
const {resolveVersion, versionFromZip} = require('../utils/version');
const {resolveVersion, versionFromArchive} = require('../utils/version');
const {rollback, zip, v1, version, force, activeVersion, instance} = context;

if (rollback) {
Expand All @@ -173,7 +173,7 @@ class UpdateCommand extends Command {

let resolvedVersion = null;
if (zip) {
resolvedVersion = await versionFromZip(zip, activeVersion, {force});
resolvedVersion = await versionFromArchive(zip, activeVersion, {force});
} else {
resolvedVersion = await resolveVersion(version, activeVersion, {
v1,
Expand Down Expand Up @@ -233,7 +233,8 @@ UpdateCommand.description = 'Update a Ghost instance';
UpdateCommand.params = '[version]';
UpdateCommand.options = {
zip: {
description: 'Path to Ghost release zip to install',
alias: 'archive',
description: 'Path to Ghost release archive to install',
type: 'string'
},
rollback: {
Expand Down
16 changes: 12 additions & 4 deletions lib/tasks/yarn-install.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
const path = require('path');
const fs = require('fs-extra');
const chalk = require('chalk');
const shasum = require('shasum');
Expand Down Expand Up @@ -67,10 +68,17 @@ const subTasks = {
}
};

module.exports = function yarnInstall(ui, zipFile) {
const tasks = zipFile ? [{
title: 'Extracting release from local zip',
task: ctx => decompress(zipFile, ctx.installPath)
module.exports = function yarnInstall(ui, archiveFile) {
const tasks = archiveFile ? [{
title: 'Extracting release from local archive file',
task: ctx => decompress(archiveFile, ctx.installPath, {
map: (file) => {
if (['.tar.gz', '.tgz'].includes(path.extname(archiveFile))) {
file.path = file.path.replace('package/', '');
}
return file;
}
})
}] : [{
title: 'Getting download information',
task: subTasks.dist
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/check-valid-install.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Otherwise, run \`ghost ${name}\` again within a valid Ghost installation.`);
fs.readJsonSync(path.join(dir, 'package.json')).name === 'ghost' &&
fs.existsSync(path.join(dir, 'Gruntfile.js'))
) {
console.error(`${chalk.yellow('Ghost-CLI commands do not work inside of a git clone, zip download or with Ghost <1.0.0.')}
console.error(`${chalk.yellow('Ghost-CLI commands do not work inside of a git clone, archive download or with Ghost <1.0.0.')}
Perhaps you meant \`grunt ${name}\`?
Otherwise, run \`ghost ${name}\` again within a valid Ghost installation.`);

Expand Down
47 changes: 33 additions & 14 deletions lib/utils/version.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const fs = require('fs');
const path = require('path');
const semver = require('semver');
const AdmZip = require('adm-zip');
const decompress = require('decompress');
const packageJson = require('package-json');

const cliPackage = require('../../package.json');
Expand Down Expand Up @@ -89,7 +89,7 @@ const utils = {
}

if (!opts.force && activeVersion && semver.lt(parsed, activeVersion)) {
const message = opts.zip ? 'Version in zip file' : 'The custom version specified';
const message = opts.zip ? 'Version in archive file' : 'The custom version specified';

throw new CliError({
message: `${message}: ${version}, is less than the current active version: ${activeVersion}`,
Expand Down Expand Up @@ -164,42 +164,61 @@ const utils = {
return utils.checkActiveVersion(activeVersion, latest, versions.latestMajor, opts);
},

async versionFromZip(zipPath, activeVersion = null, opts = {}) {
if (!path.isAbsolute(zipPath)) {
zipPath = path.join(process.cwd(), zipPath);
async versionFromArchive(archivePath, activeVersion = null, opts = {}) {
if (!path.isAbsolute(archivePath)) {
archivePath = path.join(process.cwd(), archivePath);
}

if (!fs.existsSync(zipPath) || path.extname(zipPath) !== '.zip') {
return Promise.reject(new SystemError('Zip file could not be found.'));
if (!fs.existsSync(archivePath) || !['.tar.gz', '.tgz', '.zip'].includes(path.extname(archivePath))) {
return Promise.reject(new SystemError('A supported archive file could not be found.'));
}

let files;

try {
files = await decompress(archivePath, {
filter: (file) => {
if (['.tar.gz', '.tgz'].includes(path.extname(archivePath))) {
file.path = file.path.replace('package/', '');
}

return file.path === 'package.json';
}
});
} catch (e) {
return Promise.reject(new SystemError('Archive file could not be extracted'));
}

const zip = new AdmZip(zipPath);
let pkg;

try {
pkg = JSON.parse(zip.readAsText('package.json'));
if (!files || !files.length) {
throw new Error('no files found');
}

pkg = JSON.parse(files[0].data.toString());
} catch (e) {
return Promise.reject(new SystemError('Zip file does not contain a valid package.json.'));
return Promise.reject(new SystemError('Archive file does not contain a valid package.json.'));
}

if (pkg.name !== 'ghost') {
return Promise.reject(new SystemError('Zip file does not contain a Ghost release.'));
return Promise.reject(new SystemError('Archive file does not contain a Ghost release.'));
}

if (semver.lt(pkg.version, '1.0.0')) {
return Promise.reject(new SystemError('Zip file contains pre-1.0 version of Ghost.'));
return Promise.reject(new SystemError('Archive file contains pre-1.0 version of Ghost.'));
}

if (
process.env.GHOST_NODE_VERSION_CHECK !== 'false' &&
pkg.engines && pkg.engines.node && !semver.satisfies(process.versions.node, pkg.engines.node)
) {
return Promise.reject(new SystemError('Zip file contains a Ghost version incompatible with the current Node version.'));
return Promise.reject(new SystemError('Archive file contains a Ghost version incompatible with the current Node version.'));
}

if (pkg.engines && pkg.engines.cli && !semver.satisfies(cliPackage.version, pkg.engines.cli, {includePrerelease: true})) {
return Promise.reject(new SystemError({
message: 'Zip file contains a Ghost version incompatible with this version of the CLI.',
message: 'Archive file contains a Ghost version incompatible with this version of the CLI.',
help: `Required: v${pkg.engines.cli}, current: v${cliPackage.version}`,
suggestion: 'npm install -g ghost-cli@latest'
}));
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
"dependencies": {
"@tryghost/zip": "^1.1.25",
"abbrev": "1.1.1",
"adm-zip": "0.5.9",
"bluebird": "3.7.2",
"boxen": "5.1.2",
"chalk": "4.1.2",
Expand Down
18 changes: 9 additions & 9 deletions test/unit/commands/install-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,11 @@ describe('Unit: Commands > Install', function () {
expect(context.installPath).to.equal(path.join(process.cwd(), 'versions/1.5.0'));
});

it('calls versionFromZip if zip file is passed in context', async function () {
it('calls versionFromArchive if zip file is passed in context', async function () {
const resolveVersion = sinon.stub().resolves('1.5.0');
const versionFromZip = sinon.stub().resolves('1.5.2');
const versionFromArchive = sinon.stub().resolves('1.5.2');
const InstallCommand = proxyquire(modulePath, {
'../utils/version': {resolveVersion, versionFromZip}
'../utils/version': {resolveVersion, versionFromArchive}
});
const log = sinon.stub();

Expand All @@ -318,18 +318,18 @@ describe('Unit: Commands > Install', function () {

await testInstance.version(context);
expect(resolveVersion.called).to.be.false;
expect(versionFromZip.calledOnce).to.be.true;
expect(versionFromZip.calledWith('/some/zip/file.zip')).to.be.true;
expect(versionFromArchive.calledOnce).to.be.true;
expect(versionFromArchive.calledWith('/some/zip/file.zip')).to.be.true;
expect(context.version).to.equal('1.5.2');
expect(context.installPath).to.equal(path.join(process.cwd(), 'versions/1.5.2'));
expect(log.called).to.be.false;
});

it('logs if both version and zip are passed', async function () {
const resolveVersion = sinon.stub().resolves('1.5.0');
const versionFromZip = sinon.stub().resolves('1.5.2');
const versionFromArchive = sinon.stub().resolves('1.5.2');
const InstallCommand = proxyquire(modulePath, {
'../utils/version': {resolveVersion, versionFromZip}
'../utils/version': {resolveVersion, versionFromArchive}
});
const log = sinon.stub();

Expand All @@ -338,8 +338,8 @@ describe('Unit: Commands > Install', function () {

await testInstance.version(context);
expect(resolveVersion.called).to.be.false;
expect(versionFromZip.calledOnce).to.be.true;
expect(versionFromZip.calledWith('/some/zip/file.zip')).to.be.true;
expect(versionFromArchive.calledOnce).to.be.true;
expect(versionFromArchive.calledWith('/some/zip/file.zip')).to.be.true;
expect(context.version).to.equal('1.5.2');
expect(context.installPath).to.equal(path.join(process.cwd(), 'versions/1.5.2'));
expect(log.calledOnce).to.be.true;
Expand Down
10 changes: 5 additions & 5 deletions test/unit/commands/update-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -889,11 +889,11 @@ describe('Unit: Commands > Update', function () {
expect(context.installPath).to.equal('/var/www/ghost/versions/1.0.1');
});

it('calls versionFromZip resolver with zip path if zip is passed', async function () {
it('calls versionFromArchive resolver with zip path if zip is passed', async function () {
const resolveVersion = sinon.stub().resolves('1.0.1');
const versionFromZip = sinon.stub().resolves('1.1.0');
const versionFromArchive = sinon.stub().resolves('1.1.0');
const UpdateCommand = proxyquire(modulePath, {
'../utils/version': {resolveVersion, versionFromZip}
'../utils/version': {resolveVersion, versionFromArchive}
});
const instance = new UpdateCommand({}, {});
const context = {
Expand All @@ -909,8 +909,8 @@ describe('Unit: Commands > Update', function () {
const result = await instance.version(context);
expect(result).to.be.true;
expect(resolveVersion.called).to.be.false;
expect(versionFromZip.calledOnce).to.be.true;
expect(versionFromZip.calledWithExactly('/some/zip/file.zip', '1.0.0', {force: false})).to.be.true;
expect(versionFromArchive.calledOnce).to.be.true;
expect(versionFromArchive.calledWithExactly('/some/zip/file.zip', '1.0.0', {force: false})).to.be.true;
expect(context.version).to.equal('1.1.0');
expect(context.installPath).to.equal('/var/www/ghost/versions/1.1.0');
});
Expand Down
2 changes: 1 addition & 1 deletion test/unit/tasks/yarn-install-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe('Unit: Tasks > yarn-install', function () {
tasks[0].task(ctx);

expect(decompressStub.called).to.be.true;
expect(decompressStub.calledWithExactly('test.zip','/var/www/ghost')).to.be.true;
expect(decompressStub.calledWith('test.zip','/var/www/ghost')).to.be.true;
});
});

Expand Down

0 comments on commit a079cdc

Please sign in to comment.