You rename author to creator in the schema. The schema is current immediately — but three hundred entries still carry author, and nobody notices until the frontend shows empty author lines. RustyCMS keeps the two steps deliberately apart: the schema changes by editing a file, the content changes by a migration you read before it runs.
One file per change
// <content>/_migrations/20260504-120000-rename-author-to-creator.json5
{
id: "20260504-120000-rename-author-to-creator",
description: "rename author → creator on post",
operations: [
{ type: "rename_field", collection: "post", from: "author", to: "creator" },
{ type: "delete_field", collection: "post", field: "obsolete_flag" },
],
}
Two operations: rename_field and delete_field. The migration names the collection; the CLI walks every entry in it — in every space it finds, or only the one you pass with --env. The timestamp in the filename keeps several migrations running in the right order.
Look first, write second
# Dry run (default) — prints what would change, writes nothing
migrate-schema --content-dir ./content
# Apply
migrate-schema --content-dir ./content --apply
Without --apply nothing happens. What has been applied is recorded per space in a file, so a second run skips it. A deploy script can therefore call the CLI on every run: the exit code is 0 even when there was nothing to do. After an interrupted run, --force repeats the migration.
Nothing gets clobbered
rename_field only moves the value if the target doesn’t exist yet — an entry that already has creator is left alone. Localized fields travel with their whole language map; { de, en } stays together. delete_field on a localized field removes the entire map.
Migration and schema in the same commit
The migration file belongs in the same commit as the schema change. Whoever digs for the bug later finds both side by side. A migration that has been applied anywhere is never edited — the id is compared as text, and an edited file would silently run again. A correction is a new file. And a dry run in CI catches a typo in the collection name or broken JSON5 before content is touched in production.
What the CLI can’t do
No folding several fields into one nested object, no type conversion (string → integer), no defaults for missing keys, no moves between collections, no rollback. Restructurings like that get their own script under the same rules: dry run by default, idempotent, values carried over verbatim. And migrations run from the CLI only — no endpoint, no button in the admin that rewrites a thousand files by accident.