Skip to content

Commit

Permalink
feat(v2): added a new task to help users migrating to a new major ver…
Browse files Browse the repository at this point in the history
…sion (TryGhost#771)

refs TryGhost#759
- executed new task if you are migrating from v1 to v2
  - do not execute the new task if you are migrating from v1 to v1
  - do not execute the new task if you are already on v2
- this task will:
  - scan your theme with GScan 2.0
  - load the demo post from your database
  - show a nice UI and prompts
- added unit tests
  • Loading branch information
kirrg001 authored and acburdine committed Aug 16, 2018
1 parent c674732 commit ba8a35e
Show file tree
Hide file tree
Showing 10 changed files with 663 additions and 21 deletions.
15 changes: 14 additions & 1 deletion lib/commands/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class UpdateCommand extends Command {

const MigrateCommand = require('./migrate');
const migrator = require('../tasks/migrator');
const majorUpdate = require('../tasks/major-update');

const instance = this.system.getInstance();

Expand Down Expand Up @@ -66,6 +67,18 @@ class UpdateCommand extends Command {
title: 'Downloading and updating Ghost',
skip: (ctx) => ctx.rollback,
task: this.downloadAndUpdate
}, {
title: 'Updating to a major version',
task: majorUpdate,
// CASE: Skip if you are already on ^2 or you update from v1 to v1.
enabled: () => {
if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0') ||
!semver.satisfies(context.version, '^2.0.0')) {
return false;
}

return true;
}
}, {
title: 'Stopping Ghost',
enabled: () => isRunning,
Expand All @@ -82,7 +95,7 @@ class UpdateCommand extends Command {
skip: (ctx) => ctx.rollback,
task: migrator.migrate,
// CASE: We have moved the execution of knex-migrator into Ghost 2.0.0.
// If you are already on ^2 or you update from ^1 to ^2, then skip the task.
// If you are already on v2 or you update from v1 to v2, then skip the task.
enabled: () => {
if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0') ||
semver.satisfies(context.version, '^2.0.0')) {
Expand Down
1 change: 1 addition & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class CliError extends Error {
Error.captureStackTrace(this, this.constructor);

this.options = options;
this.logMessageOnly = options.logMessageOnly;

this.help = 'Please refer to https://docs.ghost.org/v1/docs/troubleshooting#section-cli-errors for troubleshooting.'

Expand Down
75 changes: 75 additions & 0 deletions lib/tasks/major-update/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';

module.exports = function getData(options = {}) {
const path = require('path');
const errors = require('../../errors');

if (!options.dir) {
return Promise.reject(new errors.CliError({
message: '`dir` is required.'
}))
}

if (!options.database) {
return Promise.reject(new errors.CliError({
message: '`database` is required.'
}))
}

const knexPath = path.resolve(options.dir, options.version, 'node_modules', 'knex');
const gscanPath = path.resolve(options.dir, options.version, 'node_modules', 'gscan');

const knex = require(knexPath);
const gscan = require(gscanPath);

const connection = knex(Object.assign({useNullAsDefault: true}, options.database));

const themeFolder = path.resolve(options.dir, 'content', 'themes');
let gscanReport;

return connection.raw('SELECT * FROM settings WHERE `key`="active_theme";')
.then((response) => {
let activeTheme;

if (options.database.client === 'mysql') {
activeTheme = response[0][0].value;
} else {
activeTheme = response[0].value;
}

return gscan.check(path.resolve(themeFolder, activeTheme));
})
.then((report) => {
gscanReport = gscan.format(report, {sortByFiles: true});

return connection.raw('SELECT uuid FROM posts WHERE slug="v2-demo-post";')
})
.then((response) => {
let demoPost;

if (options.database.client === 'mysql') {
demoPost = response[0][0];
} else {
demoPost = response[0];
}

return {
gscanReport: gscanReport,
demoPost: demoPost
};
})
.then((response) => {
return new Promise((resolve) => {
connection.destroy(() => {
resolve(response);
});
});
})
.catch((err) => {
return new Promise((resolve, reject) => {
connection.destroy(() => {
reject(err);
});
});
});
};
2 changes: 2 additions & 0 deletions lib/tasks/major-update/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict';
module.exports = require('./ui');
160 changes: 160 additions & 0 deletions lib/tasks/major-update/ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
'use strict';

module.exports = function ui(ctx) {
const chalk = require('chalk');
const logSymbols = require('log-symbols');
const each = require('lodash/each');
const errors = require('../../errors');
const getData = require('./data');
let gscanReport;
let demoPost;

return getData({
dir: ctx.instance.dir,
database: ctx.instance.config.get('database'),
version: `versions/${ctx.version}`
}).then((response) => {
gscanReport = response.gscanReport;
demoPost = response.demoPost;

ctx.ui.log(chalk.bold.underline.white(`\n\nChecking theme compatibility for Ghost ${ctx.version}\n`));
if (!gscanReport.results.error.all.length && !gscanReport.results.warning.all.length) {
ctx.ui.log(`${logSymbols.success} Your theme is compatible.\n`);

if (demoPost && demoPost.uuid) {
const demoLink = `${ctx.instance.config.get('url')}p/${demoPost.uuid}/`;
ctx.ui.log(`Visit the demo post at ${chalk.cyan(demoLink)} to see how your theme looks like in Ghost 2.0`);
}

ctx.ui.log(`You can also check theme compatibility at ${chalk.cyan('https://gscan.ghost.org')}\n`);
} else {
let message = '';

if (gscanReport.results.warning.all.length && !gscanReport.results.error.all.length) {
message += `${chalk.yellow('⚠')} Your theme has `;

let text = 'warning';

if (gscanReport.results.warning.all.length > 1) {
text = 'warnings';
}

message += chalk.bold.yellow(`${gscanReport.results.warning.all.length} ${text}`);
} else if (!gscanReport.results.warning.all.length && gscanReport.results.error.all.length) {
message += `${chalk.red('⚠')} Your theme has `;

let text = 'error';

if (gscanReport.results.error.all.length > 1) {
text = 'errors';
}

message += chalk.bold.red(`${gscanReport.results.error.all.length} ${text}`);
} else if (gscanReport.results.warning.all.length && gscanReport.results.error.all.length) {
message += `${chalk.red('⚠')} Your theme has `;

let text1 = 'error';
let text2 = 'warning';

if (gscanReport.results.error.all.length > 1) {
text1 = 'errors';
}

if (gscanReport.results.warning.all.length > 1) {
text2 = 'warnings';
}

message += chalk.bold.red(`${gscanReport.results.error.all.length} ${text1}`);
message += ' and ';
message += chalk.bold.yellow(`${gscanReport.results.warning.all.length} ${text2}`);
}

message += '\n';
ctx.ui.log(message);

return ctx.ui.confirm('View error and warning details?', null, {prefix: chalk.cyan('?')});
}
}).then((answer) => {
if (answer) {
const spaces = ' ';

if (gscanReport.results.error.all.length) {
ctx.ui.log(chalk.bold.red('\nErrors'));

each(gscanReport.results.error.byFiles, (errors, fileName) => {
if (!errors.length) {
return;
}

let message = chalk.bold.white(`${spaces}File: `);
message += chalk.white(`${fileName}`);
message += '\n';

errors.forEach((error, index) => {
if (error.fatal) {
message += `${spaces}- ${chalk.bold.red('Fatal error:')} ${error.rule.replace(/(<([^>]+)>)/ig, '')}`;
} else {
message += `${spaces}- ${error.rule.replace(/(<([^>]+)>)/ig, '')}`;
}

if (index < (errors.length - 1)) {
message += '\n';
}
});

message += '\n';
ctx.ui.log(message);
});
}

if (gscanReport.results.warning.all.length) {
ctx.ui.log(chalk.bold.yellow('\nWarnings'));

each(gscanReport.results.warning.byFiles, (warnings, fileName) => {
if (!warnings.length) {
return;
}

let message = chalk.bold.white(`${spaces}File: `);
message += chalk.white(`${fileName}`);
message += '\n';

warnings.forEach((warning, index) => {
message += `${spaces}- ${warning.rule.replace(/(<([^>]+)>)/ig, '')}`;

if (index < (warnings.length - 1)) {
message += '\n';
}
});

message += '\n';
ctx.ui.log(message);
});
}

if (demoPost && demoPost.uuid) {
const demoLink = `${ctx.instance.config.get('url')}p/${demoPost.uuid}/`;
ctx.ui.log(`Visit the demo post at ${chalk.cyan(demoLink)} to see how your theme looks like in Ghost 2.0`);
}

ctx.ui.log(`You can also check theme compatibility at ${chalk.cyan('https://gscan.ghost.org')}\n`);
}

if (gscanReport.results.hasFatalErrors) {
return Promise.reject(new errors.CliError({
message: 'Migration failed. Your theme has fatal errors.\n For additional theme help visit https://themes.ghost.org/docs/changelog',
logMessageOnly: true
}));
}

return ctx.ui.confirm(`Are you sure you want to proceed with migrating to Ghost ${ctx.version}?`, null, {prefix: chalk.cyan('?')})
.then((answer) => {
if (!answer) {
return Promise.reject(new errors.CliError({
message: `Update aborted. Your blog is still on ${ctx.activeVersion}.`,
logMessageOnly: true
}));
}
});
});
};
9 changes: 7 additions & 2 deletions lib/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class UI {
* @method confirm
* @public
*/
confirm(question, defaultAnswer) {
confirm(question, defaultAnswer, options = {}) {
if (!this.allowPrompt) {
return Promise.resolve(defaultAnswer);
}
Expand All @@ -153,7 +153,8 @@ class UI {
type: 'confirm',
name: 'yes',
message: question,
default: defaultAnswer
default: defaultAnswer,
prefix: options.prefix
}).then((answer) => {
return answer.yes;
});
Expand Down Expand Up @@ -328,6 +329,10 @@ class UI {
const debugInfo = this._formatDebug(system);

if (error instanceof errors.CliError) {
if (error.logMessageOnly) {
return this.fail(error.message);
}

// Error is one that is generated by CLI usage (as in, the CLI itself
// manually generates this error)
this.log(`A ${error.type} occurred.\n`, 'red');
Expand Down
Loading

0 comments on commit ba8a35e

Please sign in to comment.