# Post-Update Hook and Migrations ## Overview After a successful deployment, the client runs a post-update step. It consists of two independent mechanisms, used individually or together: 1. **Migrations** – ordered, one-time scripts shipped with the release. The usual place for database changes. 2. **Project callback** – a project function that runs after every update. For clearing caches, rebuilding derived files, setting permissions. Relevant files: - `manage-client/lib/hooks.php` – both mechanisms - `MANAGE_MIGRATIONS_DIR`, `MANAGE_MIGRATIONS_STATE`, `MANAGE_UPDATE_POST_HOOK` Order: migrations first, then the callback — so the callback can rely on the new schema. If a migration fails, the callback is **not** run. ## Migrations ### Location Migrations live in the directory from `MANAGE_MIGRATIONS_DIR` (default `migrations/` in the application root) and are **shipped with the release package**. ```text migrations/ 2026-08-20-01-add-orders-index.php 2026-08-21-01-backfill-categories.php ``` Run in **filename order**. A date prefix with a running number sorts reliably. The filename without `.php` is the migration's id; renaming an already-run file makes it run again. ### Structure Recommended form — the file returns a function: ```php exec("ALTER TABLE orders ADD INDEX idx_created (created_at)"); }; ``` Alternatively, the file defines a function `up()`: ```php "/var/www/myproject", "instance" => "myproject-prod", "from_version" => "v1.2.3", "to_version" => "v1.3.0", "backup_dir" => "/…/data/manage/updates/20260820-092114-v1.3.0", "run_id" => "20260820-092114", "migration_id" => "2026-08-20-01-add-orders-index", "pdo" => PDO, // only when MANAGE_BACKUP_DATABASE is configured ] ``` `pdo` uses the credentials already configured for the database dump. A second configuration isn't needed. Projects without a database work with `app_root`. ### Example: database ```php query( "SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'orders' AND index_name = 'idx_created'" )->fetchColumn(); if ((int) $exists === 0) { $pdo->exec("ALTER TABLE orders ADD INDEX idx_created (created_at)"); } }; ``` ### Example: JSON data ```php $product) { if (!array_key_exists("category_id", $product)) { $data[$index]["category_id"] = null; } } file_put_contents( $file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ); }; ``` ### State Executed migrations are recorded in `MANAGE_MIGRATIONS_STATE`: ```json { "applied": [ { "id": "2026-08-20-01-add-orders-index", "applied_at": "2026-08-20T09:21:14+00:00", "version": "v1.3.0", "duration_ms": 42 } ] } ``` This file lives in the data directory and is therefore exempt from updates. It should be included in the backup if `data/manage/` is among the sources — by default it isn't, because the backup directory would otherwise back up itself. To include the migration state, add it individually: ```php ["as" => "manage", "file" => "data/manage/migrations.json"], ``` ## Project callback ```php define("MANAGE_UPDATE_POST_HOOK", [ "file" => MANAGE_APP_ROOT . "/includes/after-update.php", "callback" => "myProjectAfterUpdate", ]); ``` ```php false, "error" => "…"]`. Everything else counts as success. The callback receives the same context as a migration, plus `migrations` with the list of ids executed in this run. ## Failure behavior The most important point: **at this point the files are already deployed, and there is no rollback.** A failure is therefore reported loudly instead of swallowed silently. Specifically: - The run stops at the first failed migration. The following ones stay pending and are not attempted. - The callback is skipped if a migration failed. - `manageUpdateApply()` returns **normally**, with `deployed => true` and `hook["success"] => false`. - The command line exits with code `1` and names the affected file — but explicitly reports that deployment itself succeeded. - The UI shows a red notice with the migration's name. - Both are recorded in the client log. Recovery after a failure: ```bash # 1. Fix the cause (correct the migration, set permissions, check the database) # 2. Look at pending migrations php manage-client/bin/manage-client.php migrate --dry-run # 3. Catch up php manage-client/bin/manage-client.php migrate ``` Running `update --force` again redeploys the files, but **does not re-run migrations that already ran**. ## Idempotency Migrations should be safe to run more than once. Reason: if a migration fails partway through — say, after half of a data conversion — it is not recorded as applied and runs again from the start on the next `migrate`. Only an idempotent migration survives that unscathed. In practice that means: check whether the change is already there before making it, and write data conversions so that already-converted rows are skipped. ## Deploying without the post-update step ```bash php manage-client/bin/manage-client.php update --skip-hook ``` Deploys only the files. Migrations stay pending and can be caught up later with `migrate`. Useful when the files are urgently needed but the database change belongs in a maintenance window. ## Next - [06_UPDATE_PACKAGING](06_UPDATE_PACKAGING.md) – getting migrations into the package - [04_FUNCTION_API](04_FUNCTION_API.md) – `manageUpdateRunMigrations()`