Model
Four pieces, each with one job.
Registering a migration is a seed data row, not an application change.
componentName exists so sibling components can register into the same registry rather than each growing a private one.
Status values
The ledger records four statuses, and only one of them means “done”.
That
DRY_RUN never counts as applied is the property that makes previewing safe: an operator can preview a migration as often as they like without ever convincing the supervisor the work is done.
Why the ledger commits separately
record#MigrationRun runs in a forced-new transaction. A migration that fails rolls its own work back; if the ledger row were written in that same transaction it would roll back with it, and the installation would have no record that the attempt happened.
Committing the ledger independently means a failure leaves evidence behind. This is the difference between diagnosing a broken migration and rediscovering it.
Running migrations
run#PendingMigrations applies everything with no success recorded, in sequence order. It is idempotent — a second call applies nothing — which is what makes it safe to run on every deployment rather than only when someone remembers.
Three gates apply, to the sweep and to a targeted run alike:
- A parked migration (
enabled=N) is skipped. Parking exists so one broken migration does not strand every migration behind it — the rest of the sweep proceeds, and the parked one is dealt with on its own. - A migration whose prerequisites have not succeeded is refused and recorded as blocked, rather than run against the state it was not written for.
- An already-applied migration is skipped.
run#Migration runs one named migration for targeted remediation, under the same gates, except that admin.MigrationAdminServices.run#Migration with force=true re-runs one that is already applied. Upgrading to 2.0.0 needs this once for RETIRED_FIELDS, because 1.5.0’s run already marked it applied. dryRun#Migration previews one, and run#PendingMigrations with dryRun set previews the whole sweep. set#MigrationEnabled parks or unparks a migration.
Operator surface
The internal services live inservice/migration/MigrationServices.xml and are not callable remotely (allow-remote="false"). list#MigrationStatus returns registry and ledger state for every migration in sequence order, including the most recent attempt’s detail. get#MigrationHistory returns every attempt at one migration, newest first, each with its own sanitized failure detail — which is how a failed migration is diagnosed without shell access to the installation.
The admin application reaches them through admin.MigrationAdminServices, behind the DARPAN_ADMIN_API artifact group, which only super admins hold:
Preview and apply are separate method names on purpose: a dropped
dryRun flag cannot turn a preview into a write. The component also ships a Migrations screen with the status table and Dry Run All and Run Pending actions.
Failure detail is sanitized before it is stored, because it is read by an operator through a remote surface.
Registered migrations
The seed registry currently declares six, in sequence:Adding a migration
- Write the service that performs the work. Accept a
dryRunparameter if the work can be meaningfully previewed. - Add a
DarpanMigrationseed row naming the component, the service, a description an operator can decide from, and a sequence number. - Add
DarpanMigrationPrereqrows for anything that must have succeeded first. - Set
supportsDryRuntoYonly once the service genuinely honours it.
Upgrade data
Migrations translate existing rows. Seed and reference data, such as enumerations, security groups, connector rows, and the migration registry itself, ships separately as upgrade data.- Seed files under
data/*.xmlare the source of truth. Files that every installation needs declaretype="darpan-seed". data/upgrade-data.xmlis the load target for the current release. Each release’s pack, with its notes and review, is archived underdata/releases/<version>/../gradlew :runtime:component:darpan:loadDarpanUpgradeDataloads everydarpan-seedfile idempotently (types=darpan-seed). SetDARPAN_UPGRADE_DATA_LOCATIONto load one file instead.
- The
typeattribute is a gate. A load only reads rows whose filetypematches the requested types. A file with the wrong type is skipped, and the load still reports success. Check the type before relying on a load. - Foreign-key order matters. Moqui loads rows in file order. A parent row, such as a
DarpanMigration, must come before any row that references it, such as itsDarpanMigrationPrereq. Check parent-before-child order by hand when assembling a release pack from several seed files.