Error occurred while executing the following migration: 23-regenerate-posts-html.js

I’m trying to update an old installation of ghost on my Ubuntu 18.04. My old version was using:
Ghost-CLI version: 1.15.3
Ghost version: 3.42.0
mysql Ver 14.14 Distrib 5.7.33
Nodejs: v12.14.0

I initially upgraded my Node.js to version 14, as the previous version was incompatible with both ghost update v3 and the newer Ghost CLI version. Following that, I upgraded MySQL to version 8.0.33, adjusted collations like the tutorial suggested and made sure that it was working before continuing. Afterward, I further upgraded Node.js to version 16.20.2 and proceeded with the Ghost update process. Firstly, I updated Ghost CLI to version 1.24.2, and then upgraded Ghost to version 5.60.0.

When I run ghost start I get this error:
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: “Error occurred while executing the following migration: 23-regenerate-posts-html.js”
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: Error ID:
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: 300
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: ----------------------------------------
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: TypeError: Cannot read properties of undefined (reading ‘replace’)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at /var/www/ghost/versions/5.60.0/node_modules/knex-migrator/lib/index.js:1032:19
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at prepareText (/var/www/ghost/versions/5.60.0/node_modules/mobiledoc-dom-renderer/dist/commonjs/mobiledoc-dom-renderer/utils/dom.js:9:15)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at createTextNode (/var/www/ghost/versions/5.60.0/node_modules/mobiledoc-dom-renderer/dist/commonjs/mobiledoc-dom-renderer/utils/dom.js:13:29)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at Renderer.renderMarkersOnElement (/var/www/ghost/versions/5.60.0/node_modules/mobiledoc-dom-renderer/dist/commonjs/mobiledoc-dom-renderer/renderers/0-3.js:190:69)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at Renderer.renderMarkupSection (/var/www/ghost/versions/5.60.0/node_modules/mobiledoc-dom-renderer/dist/commonjs/mobiledoc-dom-renderer/renderers/0-3.js:474:12)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at Renderer.renderSection (/var/www/ghost/versions/5.60.0/node_modules/mobiledoc-dom-renderer/dist/commonjs/mobiledoc-dom-renderer/renderers/0-3.js:139:23)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at /var/www/ghost/versions/5.60.0/node_modules/mobiledoc-dom-renderer/dist/commonjs/mobiledoc-dom-renderer/renderers/0-3.js:103:31
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at Array.forEach ()
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at Renderer.render (/var/www/ghost/versions/5.60.0/node_modules/mobiledoc-dom-renderer/dist/commonjs/mobiledoc-dom-renderer/renderers/0-3.js:102:21)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at RendererFactory.render (/var/www/ghost/versions/5.60.0/node_modules/mobiledoc-dom-renderer/dist/commonjs/mobiledoc-dom-renderer/renderer-factory.js:109:71)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at MobiledocHtmlRenderer.render (/var/www/ghost/versions/5.60.0/node_modules/@tryghost/kg-mobiledoc-html-renderer/lib/MobiledocHtmlRenderer.js:116:35)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at /var/www/ghost/versions/5.60.0/core/server/data/migrations/versions/4.0/23-regenerate-posts-html.js:42:61
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: at processTicksAndRejections (node:internal/process/task_queues:96:5)
Aug 28 21:26:22 ip-172-31-36-55 node[27633]:
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: [2023-08-28 21:26:22] WARN Ghost is shutting down
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: [2023-08-28 21:26:22] WARN Ghost has shut down
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: [2023-08-28 21:26:22] WARN Your site is now offline
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: [2023-08-28 21:26:22] WARN Ghost was running for a few seconds
Aug 28 21:26:22 ip-172-31-36-55 node[27633]: [2023-08-28 21:26:22] INFO Bootstrap client was closed.
Aug 28 21:26:22 ip-172-31-36-55 systemd[1]: Stopping Ghost systemd service for blog: admin-gazillions-com…

The knex-migrator function that throws the errors is:
KnexMigrator.prototype._migrateTo = function _migrateTo(options) {
options = options || {};

let self = this,
    version = options.version,
    hooks = options.hooks || {},
    only = options.only || null,
    skip = options.skip || null,
    subfolder = this.subfolder,
    skippedTasks = [],
    tasks = [];

if (version !== 'init') {
    tasks = utils.readTasks(path.join(self.migrationPath, subfolder, version));
} else {
    try {
        tasks = utils.readTasks(path.join(self.migrationPath, version));
    } catch (err) {
        if (err.code === 'MIGRATION_PATH') {
            tasks = [];
        } else {
            throw err;
        }
    }
}

if (only !== null) {
    debug('only: ' + only);
    tasks = [tasks[only - 1]];
} else if (skip !== null) {
    debug('skip: ' + skip);
    tasks.splice(skip - 1, 1);
}

debug('Migrate: ' + version + ' with ' + tasks.length + ' tasks.');
debug('Tasks: ' + JSON.stringify(tasks));

return Promise.each(tasks, function executeTask(task) {
    return self._beforeEach({
        task: task.name,
        version: version
    }).then(function () {
        if (hooks.beforeEach) {
            return hooks.beforeEach({
                connection: self.connection
            });
        }
    }).then(function () {
        debug('Running up: ' + task.name);

        if (task.config && task.config.transaction) {
            return database.createTransaction(self.connection, function (txn) {
                return task.up({
                    transacting: txn
                });
            });
        }

        return task.up({
            connection: self.connection
        });
    }).then(function () {
        if (hooks.afterEach) {
            return hooks.afterEach({
                connection: self.connection
            });
        }
    }).then(function () {
        return self._afterEach({
            task: task,
            version: version
        });
    }).catch(function (err) {
        if (err instanceof errors.MigrationExistsError) {
            debug('Skipping:' + task.name);
            skippedTasks.push(task.name);
            return Promise.resolve();
        }

        /**
         * @NOTE: When your database encoding is set to utf8mb4 and you set a field length > 191 characters,
         * MySQL will throw an error, BUT it won't roll back the changes, because ALTER/CREATE table commands are
         * implicit commands.
         *
         * https://bugs.mysql.com/bug.php?id=28727
         * https://github.com/TryGhost/knex-migrator/issues/51
         */
        if (err.code === 'ER_TOO_LONG_KEY') {
            let match = err.message.match(/`\w+`/g);
            let table = match[0];
            let field = match[2];

            throw new errors.MigrationScriptError({
                message: 'Field length of %field% in %table% is too long!'.replace('%field%', field).replace('%table%', table),
                context: 'This usually happens if your database encoding is utf8mb4.\n' +
                    'All unique fields and indexes must be lower than 191 characters.\n' +
                    'Please correct your field length and reset your database with `yarn knex-migrator reset`.\n',
                help: 'Read more here: https://github.com/TryGhost/knex-migrator/issues/51\n',
                err: err
            });
        }

        throw new errors.MigrationScriptError({
            message: err.message,
            help: 'Error occurred while executing the following migration: ' + task.name,
            context: task,
            err: err
        });
    });
}).then(function () {
    return {
        skippedTasks: skippedTasks
    };
});

};

I have checked my tables, and my collations are right, all set to utf8mb4_0900_ai_ci. What else could be the problem?

In the end my problem is that one of my posts, posts_authors or posts_tags had something that caused the migration to fail, probably when using the mobiledoc-dom-renderer library. The bad news is that I have around 700 posts, and no easy way to find out which one is causing the problem at the moment

The problem was really happening in the mobiledoc-dom-renderer.js, more specifically in this function:

function prepareText(text) {
return text.replace(/ /g, ’ ’ + NBSP).replace(/\t/g, EMSP);
}

I could track down that the problem was in the mobiledoc column in my posts table. I had a few 'sections` inside the mobiledoc object that didn’t have a text, they were just [1,““p””,[[0,,0]]]. Adding a empty string after the last 0 fixed the problem for me. Or change the function to don’t try to replace if there is no text:

function prepareText(text) {
return text ? text.replace(/ /g, ’ ’ + NBSP).replace(/\t/g, EMSP) : “”;
}

If anyone needs it, heres the function I used to fix my database:

UPDATEposts
SET mobiledoc = REPLACE(mobiledoc, ‘[0,,0]’, ‘[0,,0,“”]’)
WHERE mobiledoc LIKE ‘%[0,,0]%’;