Skip to content

There is no moment at which "this installation is up to date" is true #346

Description

@BechsteinDigital

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:

  1. 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.
  2. 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.
  3. A single command with a stated contract, so "is this site migrated?" has an answer.

Proposal

  1. 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.
  2. Write a touched-relations manifest per run — plugin schemas and tables — as an artifact an operator can act on.
  3. 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.
  4. Expose it as a callora CLI verb and through the operator API, so it is scriptable in a deployment.

Done when

  • One command migrates every installed plugin in a deterministic order and reports a single pass/fail verdict
  • A test asserts ordering follows the dependency graph, not activation order
  • The touched-relations manifest is written and covers a multi-plugin run; a test pins its content
  • Backend conflict detection exists and reports before schema changes are applied; a test with two colliding test plugins pins the warning
  • docs-site/maintainer/migration-and-rollback.md documents the sequence, and a documentation gate under tests/Callora.Core.Tests/Documentation/ keeps the described order honest

Prior art

frappe/migrate.py (SiteMigration, atomic, pre_schema_updates).

Found while reviewing frappe/frappe against this repository.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    .NETPull requests that update .NET codeenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions