Ghost CLI version downgrade leads to broken install

`ghost update` keeps 2 most current, not 2 last active versions, breaking downgrading

Ghost CLI version downgrade leads to broken install
Play this article

Summary

The Ghost CLI removes old versions while updating. In the easiest-to-find search results, it is documented or mentioned, that the last 5 versions are kept, e.g.:

That includes the most current version, that was likely just installed through the update. So only 4 "old" versions are kept.

This has been changed in version 1.15.0 with no mention in the short changelog through the commit badb662, so I just missed it.

Since then, only the 2 most current versions are kept, because of disk space concerns: #201 (comment)

As this includes the most current version, only 1 "old" version is kept.

Checking the code, it seems like the version removal feature only takes Semver sorting into account:

Lines 149 to 164 of update.js:

async removeOldVersions({instance}, task) {
    const semver = require('semver');

    const versionDirs = await fs.readdir(path.join(instance.dir, 'versions'));
    const versions = versionDirs.filter(semver.valid).sort(semver.compare);

    if (versions.length <= 2) {
        task.skip();
        return;
    }

    const promises = versions.slice(0, -2)
        .map(version => fs.remove(path.join(instance.dir, 'versions', version)));

    await Promise.all(promises);
}

It does not check for the currently active or just installed version or sort the version by last use.

This leads to the unfortunate scenario, of just having downgraded to an older version and Ghost not being able to start, as it was promptly removed again and the current symlink points to a version directory that does not exist.

Steps to Reproduce

  1. Have a Ghost install with the 2 most current versions installed

  2. Run ghost update with an older version, like ghost update 5.2.0 --force

  3. Observe, that old Ghost versions are being removed

  4. Try to start Ghost and see errors or check the current symlink pointing to a version directory that does not exist.

ghost update 5.2.0 --force CLI command log:

$ ghost update 5.2.0 --force

Love open source? We’re hiring JavaScript Engineers to work on Ghost full-time.
https://careers.ghost.org

Running in development mode

✔ Checking system Node.js version - found v16.18.1
✔ Ensuring user is not logged in as ghost user
✔ Checking if logged in user is directory owner
✔ Checking current folder permissions
✔ Checking memory availability
✔ Checking free space
✔ Checking for available migrations
✔ Checking for latest Ghost version
✔ Release notes were not found
✔ Downloading and updating Ghost to v5.2.0
✔ Linking latest Ghost and recording versions
✔ Removing old Ghost versions
ghost update 5.2.0 --force  15.27s user 9.01s system 137% cpu 17.708 total
💡
The version the update command just downgraded to, should not be removed.

Possible solutions, not mutually exclusive

Always keep the current active version

Add a check, to never delete the currently active version or exclude it from the removal feature outright.
The latter would increase the number of kept versions to 3 if the feature is not adjusted for that.

Add a config option and/or CLI flag for the number of versions to keep

The change from keeping 5 versions to just 2 went by unnoticed, and it would have been great if users/admins could define the number themselves. Everybody has different infrastructure, storage requirements and rollback needs.
This would be great as a config option as well as a CLI command flag.

Keep the last active versions

Instead of just keeping the latest versions according to Semver, it would be cleaner to keep track of the active and used versions. People might not just upgrade to newer versions, but jump around.
Keeping previous versions according to the history of used versions would simplify things a lot.

GitHub issue:

Workarounds, tips and hints

Make sure to have a "free slot" for keeping versions available. With only 2 versions being kept, this means removing all (typically 1) but the most current one. That way, the feature won't be triggered, and the downgrade version is not deleted.

Did you find this article valuable?

Support coders.fail writings by becoming a sponsor. Any amount is appreciated!