Finding
Each plugin migrates itself when it activates, under its own Postgres advisory lock derived from its pluginId (PluginDbContextFactory<TContext>.MigrateAsync). That is correct in isolation and races nothing.
What it does not give us:
- No ordering across plugins. Activation order is the only order there is.
- No single point of truth. An operator upgrading a host cannot run one command and know the installation is consistent; they activate plugins and hope.
- No record of what was touched. A migration that goes wrong leaves us restoring a full backup, because nothing wrote down which tables the run modified.
- No pre-flight conflict check. Two plugins fighting over the same overridable service is discovered at runtime, if at all.
What Frappe does
frappe/migrate.py defines SiteMigration as a named, documented sequence — before_migrate hooks → pre-model-sync patches → schema → post-model-sync patches → dashboards → jobs → fixtures → customizations → languages → www pages → after_migrate — with each step wrapped in an @atomic decorator that commits or rolls back.
Three details are directly transferable:
touched_tables.json — the run writes down which tables it modified. A targeted backup instead of a full one, and a much shorter list to check when something looks wrong afterwards.
- Conflict warning before the schema step —
pre_schema_updates collects override_doctype_class across all installed apps and warns: "The controller for X is overridden by multiple apps: a, b." Detected before anything is written, not after.
- A single command with a stated contract, so "is this site migrated?" has an answer.
Proposal
- A host-level migration orchestrator that runs every installed plugin's pending migrations in a deterministic, dependency-aware order (we already resolve
dependencies and requiresCapabilities at install; reuse that graph), records the outcome per plugin, and reports one verdict.
- Write a touched-relations manifest per run — plugin schemas and tables — as an artifact an operator can act on.
- Run the conflict check before any schema work: two plugins claiming the same replaceable extension point, or the same overridable service. We already have this shape in the Admin frontend as
getServiceConflicts() (src/core/extensions/services.ts); the backend has no equivalent.
- Expose it as a
callora CLI verb and through the operator API, so it is scriptable in a deployment.
Done when
Prior art
frappe/migrate.py (SiteMigration, atomic, pre_schema_updates).
Found while reviewing frappe/frappe against this repository.
Finding
Each plugin migrates itself when it activates, under its own Postgres advisory lock derived from its
pluginId(PluginDbContextFactory<TContext>.MigrateAsync). That is correct in isolation and races nothing.What it does not give us:
What Frappe does
frappe/migrate.pydefinesSiteMigrationas a named, documented sequence —before_migratehooks → pre-model-sync patches → schema → post-model-sync patches → dashboards → jobs → fixtures → customizations → languages → www pages →after_migrate— with each step wrapped in an@atomicdecorator that commits or rolls back.Three details are directly transferable:
touched_tables.json— the run writes down which tables it modified. A targeted backup instead of a full one, and a much shorter list to check when something looks wrong afterwards.pre_schema_updatescollectsoverride_doctype_classacross all installed apps and warns: "The controller for X is overridden by multiple apps: a, b." Detected before anything is written, not after.Proposal
dependenciesandrequiresCapabilitiesat install; reuse that graph), records the outcome per plugin, and reports one verdict.getServiceConflicts()(src/core/extensions/services.ts); the backend has no equivalent.calloraCLI verb and through the operator API, so it is scriptable in a deployment.Done when
docs-site/maintainer/migration-and-rollback.mddocuments the sequence, and a documentation gate undertests/Callora.Core.Tests/Documentation/keeps the described order honestPrior art
frappe/migrate.py(SiteMigration,atomic,pre_schema_updates).Found while reviewing frappe/frappe against this repository.