Skip to content

Commit e9ed36b

Browse files
kirrg001acburdine
authored andcommitted
feat(v2): added v2 migrate task (#771)
refs #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
1 parent 8093274 commit e9ed36b

File tree

10 files changed

+663
-21
lines changed

10 files changed

+663
-21
lines changed

lib/commands/update.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class UpdateCommand extends Command {
1919

2020
const MigrateCommand = require('./migrate');
2121
const migrator = require('../tasks/migrator');
22+
const majorUpdate = require('../tasks/major-update');
2223

2324
const instance = this.system.getInstance();
2425

@@ -66,6 +67,18 @@ class UpdateCommand extends Command {
6667
title: 'Downloading and updating Ghost',
6768
skip: (ctx) => ctx.rollback,
6869
task: this.downloadAndUpdate
70+
}, {
71+
title: 'Updating to a major version',
72+
task: majorUpdate,
73+
// CASE: Skip if you are already on ^2 or you update from v1 to v1.
74+
enabled: () => {
75+
if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0') ||
76+
!semver.satisfies(context.version, '^2.0.0')) {
77+
return false;
78+
}
79+
80+
return true;
81+
}
6982
}, {
7083
title: 'Stopping Ghost',
7184
enabled: () => isRunning,
@@ -82,7 +95,7 @@ class UpdateCommand extends Command {
8295
skip: (ctx) => ctx.rollback,
8396
task: migrator.migrate,
8497
// CASE: We have moved the execution of knex-migrator into Ghost 2.0.0.
85-
// If you are already on ^2 or you update from ^1 to ^2, then skip the task.
98+
// If you are already on v2 or you update from v1 to v2, then skip the task.
8699
enabled: () => {
87100
if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0') ||
88101
semver.satisfies(context.version, '^2.0.0')) {

lib/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class CliError extends Error {
2525
Error.captureStackTrace(this, this.constructor);
2626

2727
this.options = options;
28+
this.logMessageOnly = options.logMessageOnly;
2829

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

lib/tasks/major-update/data.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
module.exports = function getData(options = {}) {
4+
const path = require('path');
5+
const errors = require('../../errors');
6+
7+
if (!options.dir) {
8+
return Promise.reject(new errors.CliError({
9+
message: '`dir` is required.'
10+
}))
11+
}
12+
13+
if (!options.database) {
14+
return Promise.reject(new errors.CliError({
15+
message: '`database` is required.'
16+
}))
17+
}
18+
19+
const knexPath = path.resolve(options.dir, options.version, 'node_modules', 'knex');
20+
const gscanPath = path.resolve(options.dir, options.version, 'node_modules', 'gscan');
21+
22+
const knex = require(knexPath);
23+
const gscan = require(gscanPath);
24+
25+
const connection = knex(Object.assign({useNullAsDefault: true}, options.database));
26+
27+
const themeFolder = path.resolve(options.dir, 'content', 'themes');
28+
let gscanReport;
29+
30+
return connection.raw('SELECT * FROM settings WHERE `key`="active_theme";')
31+
.then((response) => {
32+
let activeTheme;
33+
34+
if (options.database.client === 'mysql') {
35+
activeTheme = response[0][0].value;
36+
} else {
37+
activeTheme = response[0].value;
38+
}
39+
40+
return gscan.check(path.resolve(themeFolder, activeTheme));
41+
})
42+
.then((report) => {
43+
gscanReport = gscan.format(report, {sortByFiles: true});
44+
45+
return connection.raw('SELECT uuid FROM posts WHERE slug="v2-demo-post";')
46+
})
47+
.then((response) => {
48+
let demoPost;
49+
50+
if (options.database.client === 'mysql') {
51+
demoPost = response[0][0];
52+
} else {
53+
demoPost = response[0];
54+
}
55+
56+
return {
57+
gscanReport: gscanReport,
58+
demoPost: demoPost
59+
};
60+
})
61+
.then((response) => {
62+
return new Promise((resolve) => {
63+
connection.destroy(() => {
64+
resolve(response);
65+
});
66+
});
67+
})
68+
.catch((err) => {
69+
return new Promise((resolve, reject) => {
70+
connection.destroy(() => {
71+
reject(err);
72+
});
73+
});
74+
});
75+
};

lib/tasks/major-update/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
'use strict';
2+
module.exports = require('./ui');

lib/tasks/major-update/ui.js

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
'use strict';
2+
3+
module.exports = function ui(ctx) {
4+
const chalk = require('chalk');
5+
const logSymbols = require('log-symbols');
6+
const each = require('lodash/each');
7+
const errors = require('../../errors');
8+
const getData = require('./data');
9+
let gscanReport;
10+
let demoPost;
11+
12+
return getData({
13+
dir: ctx.instance.dir,
14+
database: ctx.instance.config.get('database'),
15+
version: `versions/${ctx.version}`
16+
}).then((response) => {
17+
gscanReport = response.gscanReport;
18+
demoPost = response.demoPost;
19+
20+
ctx.ui.log(chalk.bold.underline.white(`\n\nChecking theme compatibility for Ghost ${ctx.version}\n`));
21+
if (!gscanReport.results.error.all.length && !gscanReport.results.warning.all.length) {
22+
ctx.ui.log(`${logSymbols.success} Your theme is compatible.\n`);
23+
24+
if (demoPost && demoPost.uuid) {
25+
const demoLink = `${ctx.instance.config.get('url')}p/${demoPost.uuid}/`;
26+
ctx.ui.log(`Visit the demo post at ${chalk.cyan(demoLink)} to see how your theme looks like in Ghost 2.0`);
27+
}
28+
29+
ctx.ui.log(`You can also check theme compatibility at ${chalk.cyan('https://gscan.ghost.org')}\n`);
30+
} else {
31+
let message = '';
32+
33+
if (gscanReport.results.warning.all.length && !gscanReport.results.error.all.length) {
34+
message += `${chalk.yellow('⚠')} Your theme has `;
35+
36+
let text = 'warning';
37+
38+
if (gscanReport.results.warning.all.length > 1) {
39+
text = 'warnings';
40+
}
41+
42+
message += chalk.bold.yellow(`${gscanReport.results.warning.all.length} ${text}`);
43+
} else if (!gscanReport.results.warning.all.length && gscanReport.results.error.all.length) {
44+
message += `${chalk.red('⚠')} Your theme has `;
45+
46+
let text = 'error';
47+
48+
if (gscanReport.results.error.all.length > 1) {
49+
text = 'errors';
50+
}
51+
52+
message += chalk.bold.red(`${gscanReport.results.error.all.length} ${text}`);
53+
} else if (gscanReport.results.warning.all.length && gscanReport.results.error.all.length) {
54+
message += `${chalk.red('⚠')} Your theme has `;
55+
56+
let text1 = 'error';
57+
let text2 = 'warning';
58+
59+
if (gscanReport.results.error.all.length > 1) {
60+
text1 = 'errors';
61+
}
62+
63+
if (gscanReport.results.warning.all.length > 1) {
64+
text2 = 'warnings';
65+
}
66+
67+
message += chalk.bold.red(`${gscanReport.results.error.all.length} ${text1}`);
68+
message += ' and ';
69+
message += chalk.bold.yellow(`${gscanReport.results.warning.all.length} ${text2}`);
70+
}
71+
72+
message += '\n';
73+
ctx.ui.log(message);
74+
75+
return ctx.ui.confirm('View error and warning details?', null, {prefix: chalk.cyan('?')});
76+
}
77+
}).then((answer) => {
78+
if (answer) {
79+
const spaces = ' ';
80+
81+
if (gscanReport.results.error.all.length) {
82+
ctx.ui.log(chalk.bold.red('\nErrors'));
83+
84+
each(gscanReport.results.error.byFiles, (errors, fileName) => {
85+
if (!errors.length) {
86+
return;
87+
}
88+
89+
let message = chalk.bold.white(`${spaces}File: `);
90+
message += chalk.white(`${fileName}`);
91+
message += '\n';
92+
93+
errors.forEach((error, index) => {
94+
if (error.fatal) {
95+
message += `${spaces}- ${chalk.bold.red('Fatal error:')} ${error.rule.replace(/(<([^>]+)>)/ig, '')}`;
96+
} else {
97+
message += `${spaces}- ${error.rule.replace(/(<([^>]+)>)/ig, '')}`;
98+
}
99+
100+
if (index < (errors.length - 1)) {
101+
message += '\n';
102+
}
103+
});
104+
105+
message += '\n';
106+
ctx.ui.log(message);
107+
});
108+
}
109+
110+
if (gscanReport.results.warning.all.length) {
111+
ctx.ui.log(chalk.bold.yellow('\nWarnings'));
112+
113+
each(gscanReport.results.warning.byFiles, (warnings, fileName) => {
114+
if (!warnings.length) {
115+
return;
116+
}
117+
118+
let message = chalk.bold.white(`${spaces}File: `);
119+
message += chalk.white(`${fileName}`);
120+
message += '\n';
121+
122+
warnings.forEach((warning, index) => {
123+
message += `${spaces}- ${warning.rule.replace(/(<([^>]+)>)/ig, '')}`;
124+
125+
if (index < (warnings.length - 1)) {
126+
message += '\n';
127+
}
128+
});
129+
130+
message += '\n';
131+
ctx.ui.log(message);
132+
});
133+
}
134+
135+
if (demoPost && demoPost.uuid) {
136+
const demoLink = `${ctx.instance.config.get('url')}p/${demoPost.uuid}/`;
137+
ctx.ui.log(`Visit the demo post at ${chalk.cyan(demoLink)} to see how your theme looks like in Ghost 2.0`);
138+
}
139+
140+
ctx.ui.log(`You can also check theme compatibility at ${chalk.cyan('https://gscan.ghost.org')}\n`);
141+
}
142+
143+
if (gscanReport.results.hasFatalErrors) {
144+
return Promise.reject(new errors.CliError({
145+
message: 'Migration failed. Your theme has fatal errors.\n For additional theme help visit https://themes.ghost.org/docs/changelog',
146+
logMessageOnly: true
147+
}));
148+
}
149+
150+
return ctx.ui.confirm(`Are you sure you want to proceed with migrating to Ghost ${ctx.version}?`, null, {prefix: chalk.cyan('?')})
151+
.then((answer) => {
152+
if (!answer) {
153+
return Promise.reject(new errors.CliError({
154+
message: `Update aborted. Your blog is still on ${ctx.activeVersion}.`,
155+
logMessageOnly: true
156+
}));
157+
}
158+
});
159+
});
160+
};

lib/ui/index.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ class UI {
144144
* @method confirm
145145
* @public
146146
*/
147-
confirm(question, defaultAnswer) {
147+
confirm(question, defaultAnswer, options = {}) {
148148
if (!this.allowPrompt) {
149149
return Promise.resolve(defaultAnswer);
150150
}
@@ -153,7 +153,8 @@ class UI {
153153
type: 'confirm',
154154
name: 'yes',
155155
message: question,
156-
default: defaultAnswer
156+
default: defaultAnswer,
157+
prefix: options.prefix
157158
}).then((answer) => {
158159
return answer.yes;
159160
});
@@ -328,6 +329,10 @@ class UI {
328329
const debugInfo = this._formatDebug(system);
329330

330331
if (error instanceof errors.CliError) {
332+
if (error.logMessageOnly) {
333+
return this.fail(error.message);
334+
}
335+
331336
// Error is one that is generated by CLI usage (as in, the CLI itself
332337
// manually generates this error)
333338
this.log(`A ${error.type} occurred.\n`, 'red');

0 commit comments

Comments
 (0)