Release synchronization

dev.py release:sync applies the pending migrations and runs the release seeders, then records what it did. Run it on every deploy — it is idempotent and does nothing when there is nothing to do.

Why it exists

A migration creates a column; it does not create the rows the system needs to serve. Those are born in a seeder, and a seeder that only runs when someone remembers leaves the screen empty after a deploy on an environment nobody opens a terminal on. release:sync does both, in order, in one step.

The VERSION file

VERSION at the application root carries the release identity the deployed code must match, MAJOR.MINOR.PATCH-rNNNNN. release:sync refuses to run when the file disagrees with the running code (RELEASE_VERSION_DRIFT) or is missing (RELEASE_VERSION_FILE_MISSING): a mismatch is a stale image or a half-finished release, and migrating a node from code nobody can name is how a schema moves without a record.

The trail

Every run appends one row to release_syncs — the version, the migration and seeder counts, the status and the duration. Nothing is deleted: the previous row is the proof of which release left the database in its current state. release:status reads it back, alongside the running identity and the VERSION file.

Commands

python dev.py release:status   # identity, VERSION file, last sync
python dev.py release:sync     # migrate, seed, record

On PostgreSQL the whole sync runs under the migrator's advisory lock (craft:migrations), so two containers starting at once cannot migrate the same node concurrently. On a driver without advisory locks it runs unlocked.