From 8fabb881e23bb36ade18c805c90ef7398d264bb2 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Tue, 8 Sep 2026 11:24:30 +0200 Subject: [PATCH 1/4] feat: migrate db-sync-tool/file-sync-tool integration to php-sync-tool --- deployer/dev/task/dump.php | 9 +++- deployer/dev/task/import.php | 4 +- deployer/dev/task/sync.php | 4 +- deployer/feature/config/set.php | 10 +++- deployer/feature/task/feature_sync.php | 37 +++++++++------ deployer/functions.php | 66 ++++++++++++++++++++++++++ deployer/sync/config/set.php | 6 ++- deployer/sync/task/database_backup.php | 11 +++-- 8 files changed, 123 insertions(+), 24 deletions(-) diff --git a/deployer/dev/task/dump.php b/deployer/dev/task/dump.php index d8f81d1..ddcd606 100644 --- a/deployer/dev/task/dump.php +++ b/deployer/dev/task/dump.php @@ -18,7 +18,14 @@ $dbSyncToolOriginPath = str_replace('', $target, get('dev_db_sync_tool_origin_path')); $additionalOptions = "--origin-path $dbSyncToolOriginPath"; + $dbSyncTool = requireSyncTool('dump'); + + // php-sync-tool has no -kd/-dn equivalent yet (konradmichalik/php-sync-tool#33); until it + // does, the dump lands wherever the project's own sync-tool YAML config's "dump_dir" says, + // not necessarily $dbDumpDir + $dumpLocationOptions = usingPhpSyncTool($dbSyncTool) ? '' : "-kd $dbDumpDir -dn $dbDumpFilename"; + $dbSyncToolConfigPath = get('dev_db_sync_tool_config_path'); - runLocally("db_sync_tool -f $dbSyncToolConfigPath/$dbSyncToolSync -y -kd $dbDumpDir -dn $dbDumpFilename $additionalOptions", ['real_time_output' => true]); + runLocally("$dbSyncTool -f $dbSyncToolConfigPath/$dbSyncToolSync -y $dumpLocationOptions $additionalOptions", ['real_time_output' => true]); }) ->desc('Sync database with drush'); diff --git a/deployer/dev/task/import.php b/deployer/dev/task/import.php index c4b79ed..8f22450 100644 --- a/deployer/dev/task/import.php +++ b/deployer/dev/task/import.php @@ -8,7 +8,9 @@ $dbSyncToolSync = get('dev_db_sync_tool_default_sync'); + $dbSyncTool = requireSyncTool('import'); + $dbSyncToolConfigPath = get('dev_db_sync_tool_config_path'); - runLocally("db_sync_tool -f $dbSyncToolConfigPath/$dbSyncToolSync -y -i $dbDumpDir/$dbDumpFilename.sql", ['real_time_output' => true]); + runLocally("$dbSyncTool -f $dbSyncToolConfigPath/$dbSyncToolSync -y -i $dbDumpDir/$dbDumpFilename.sql", ['real_time_output' => true]); }) ->desc('Sync database with drush'); diff --git a/deployer/dev/task/sync.php b/deployer/dev/task/sync.php index 7dcbb43..28e2c4d 100644 --- a/deployer/dev/task/sync.php +++ b/deployer/dev/task/sync.php @@ -47,8 +47,10 @@ info("sync database from remote: $target"); + $dbSyncTool = requireSyncTool('sync'); + $dbSyncToolConfigPath = get('dev_db_sync_tool_config_path'); - runLocally("db_sync_tool -f $dbSyncToolConfigPath/$dbSyncToolSync -y $additionalOptions", ['real_time_output' => true]); + runLocally("$dbSyncTool -f $dbSyncToolConfigPath/$dbSyncToolSync -y $additionalOptions", ['real_time_output' => true]); info("💽 Database from $target synced successfully"); }) ->desc('Sync database with db-sync-tool'); diff --git a/deployer/feature/config/set.php b/deployer/feature/config/set.php index 0f6d238..8e9ebe2 100644 --- a/deployer/feature/config/set.php +++ b/deployer/feature/config/set.php @@ -23,8 +23,14 @@ * Feature Sync */ #set('feature_sync_config', null); -set('db_sync_tool', 'db_sync_tool'); # set to false, to disable db sync -set('file_sync_tool', 'file_sync_tool'); # set to false, to disable file sync +// resolves to vendor/bin/sync-tool (php-sync-tool) when available locally, falling back +// to the legacy db_sync_tool PATH binary; set to false, to disable db sync +set('db_sync_tool', function () { + return resolveSyncTool('db_sync_tool'); +}); +// legacy-only: php-sync-tool syncs files via --with-files on the db_sync_tool call above, +// so this is unused once db_sync_tool resolves to it; set to false, to disable file sync +set('file_sync_tool', 'file_sync_tool'); set('feature_sync_target_path', null); diff --git a/deployer/feature/task/feature_sync.php b/deployer/feature/task/feature_sync.php index d561691..e016989 100644 --- a/deployer/feature/task/feature_sync.php +++ b/deployer/feature/task/feature_sync.php @@ -120,35 +120,44 @@ function resolveDatabaseHostToIp(string $hostname): void $optionalVerbose = isVerbose() ? '-v' : ''; /* - * db_sync_tool + * db_sync_tool / php-sync-tool * https://github.com/jackd248/db-sync-tool + * https://github.com/konradmichalik/php-sync-tool */ - if (get('db_sync_tool') !== false) { - if (commandExistLocally("{{db_sync_tool}}")) { + $dbSyncTool = get('db_sync_tool'); + $isPhpSyncTool = false !== $dbSyncTool && usingPhpSyncTool($dbSyncTool); + + if (false !== $dbSyncTool) { + if (syncToolAvailableLocally($dbSyncTool)) { + $useRsync = $isPhpSyncTool ? '' : '--use-rsync'; + // php-sync-tool has file sync integrated, no separate file_sync_tool call needed + $withFiles = $isPhpSyncTool && false !== get('file_sync_tool') ? '--with-files' : ''; info('Synching database'); - runLocally("{{db_sync_tool}} -f {{feature_sync_config}} --target-path {{feature_sync_target_path}} --use-rsync -y $optionalVerbose"); + runLocally("$dbSyncTool -f {{feature_sync_config}} --target-path {{feature_sync_target_path}} $useRsync $withFiles -y $optionalVerbose"); $synced = true; } else { - debug("Skipping database sync, command \”{{db_sync_tool}}\” not available"); + debug("Skipping database sync, command \”$dbSyncTool\” not available"); } } else { debug("Skipping database sync, db_sync_tool was disabled"); } /* - * file_sync_tool + * file_sync_tool (legacy only, see above) * https://github.com/jackd248/file-sync-tool */ - if (get('file_sync_tool') !== false) { - if (commandExistLocally("{{file_sync_tool}}")) { - info('Synching files'); - runLocally("{{file_sync_tool}} -f {{feature_sync_config}} --files-target {{feature_sync_target_path_files}} $optionalVerbose"); - $synced = true; + if (!$isPhpSyncTool) { + if (get('file_sync_tool') !== false) { + if (commandExistLocally("{{file_sync_tool}}")) { + info('Synching files'); + runLocally("{{file_sync_tool}} -f {{feature_sync_config}} --files-target {{feature_sync_target_path_files}} $optionalVerbose"); + $synced = true; + } else { + debug("Skipping file sync, command \”{{file_sync_tool}}\” not available"); + } } else { - debug("Skipping file sync, command \”{{file_sync_tool}}\” not available"); + debug("Skipping file sync, file_sync_tool was disabled"); } - } else { - debug("Skipping file sync, file_sync_tool was disabled"); } if ($synced) info("feature branch $feature was successfully synced"); diff --git a/deployer/functions.php b/deployer/functions.php index f9ea253..62ce94a 100644 --- a/deployer/functions.php +++ b/deployer/functions.php @@ -131,6 +131,72 @@ function commandExistLocally(string $command): bool return testLocally("hash $command 2>/dev/null"); } +/** + * Checks whether a local path is executable. Unlike commandExistLocally()/hash (built + * for PATH lookups), this reliably resolves a relative path containing a slash, e.g. + * vendor/bin/sync-tool. + */ +function isExecutableLocally(string $path): bool +{ + return testLocally("[ -x $path ]"); +} + +/** + * Resolves the sync tool binary to use: prefers the Composer-installed php-sync-tool + * (https://github.com/konradmichalik/php-sync-tool) when present locally, falling back + * to the legacy PATH-installed Python tool (db-sync-tool/file-sync-tool) otherwise. + * + * Lets each downstream project migrate independently by running + * `composer require --dev konradmichalik/php-sync-tool` - no deploy.php opt-in needed. + */ +function resolveSyncTool(string $legacyBinary, string $phpBinary = 'vendor/bin/sync-tool'): string +{ + return isExecutableLocally($phpBinary) ? $phpBinary : $legacyBinary; +} + +/** + * Whether a resolved sync tool binary is php-sync-tool rather than the legacy tool: + * a path containing a slash, as opposed to a bare PATH command. + */ +function usingPhpSyncTool(string $resolvedBinary): bool +{ + return str_contains($resolvedBinary, '/'); +} + +/** + * Checks whether a resolved sync tool binary is actually available locally: a path + * (php-sync-tool) via isExecutableLocally(), a bare PATH command (the legacy tool) via + * commandExistLocally(). + */ +function syncToolAvailableLocally(string $binary): bool +{ + return usingPhpSyncTool($binary) + ? isExecutableLocally($binary) + : commandExistLocally($binary); +} + +/** + * Resolves the sync tool binary and verifies it is actually available locally, or + * throws. Used by the dev:* tasks, which have no "disabled" concept and must hard-fail + * rather than silently skip when the tool is missing. + * + * @throws \RuntimeException if db_sync_tool was disabled or is not available locally + */ +function requireSyncTool(string $action): string +{ + $dbSyncTool = get('db_sync_tool'); + + if (false === $dbSyncTool) { + throw new \RuntimeException("db_sync_tool was disabled, cannot $action."); + } + + if (!syncToolAvailableLocally($dbSyncTool)) { + throw new \RuntimeException("Sync tool \"$dbSyncTool\" not available locally."); + } + + return $dbSyncTool; +} + /** * Runs a remote command with the possibility to overwrite the default command options */ diff --git a/deployer/sync/config/set.php b/deployer/sync/config/set.php index 35fe856..66f6656 100644 --- a/deployer/sync/config/set.php +++ b/deployer/sync/config/set.php @@ -2,5 +2,9 @@ namespace Deployer; -set('db_sync_tool', 'db_sync_tool'); # set to false, to disable db backup +// resolves to vendor/bin/sync-tool (php-sync-tool) when available locally, falling back +// to the legacy db_sync_tool PATH binary; set to false, to disable db backup +set('db_sync_tool', function () { + return resolveSyncTool('db_sync_tool'); +}); #set('sync_database_backup_config', null); diff --git a/deployer/sync/task/database_backup.php b/deployer/sync/task/database_backup.php index 6660ecc..4b10a66 100644 --- a/deployer/sync/task/database_backup.php +++ b/deployer/sync/task/database_backup.php @@ -6,16 +6,19 @@ $optionalVerbose = isVerbose() ? '-v' : ''; - if (false === get('db_sync_tool')) { + $dbSyncTool = get('db_sync_tool'); + + if (false === $dbSyncTool) { debug('Skipping database backup, db_sync_tool was disabled'); return; } - if (commandExistLocally("{{db_sync_tool}}")) { + if (syncToolAvailableLocally($dbSyncTool)) { + $useRsync = usingPhpSyncTool($dbSyncTool) ? '' : '--use-rsync'; info('Generating a database backup'); - runLocally("{{db_sync_tool}} -f {{sync_database_backup_config}} --use-rsync -y $optionalVerbose"); + runLocally("$dbSyncTool -f {{sync_database_backup_config}} $useRsync -y $optionalVerbose"); } else { - debug("Skipping database backup, {{db_sync_tool}} not available"); + debug("Skipping database backup, $dbSyncTool not available"); } }) From acd1a95f17faa87df4783e6cc36b9b8ae7189974 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Tue, 8 Sep 2026 11:24:32 +0200 Subject: [PATCH 2/4] docs: document php-sync-tool as preferred sync tool --- docs/FEATURE.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/FEATURE.md b/docs/FEATURE.md index fd091ba..b5a9a3d 100644 --- a/docs/FEATURE.md +++ b/docs/FEATURE.md @@ -39,7 +39,13 @@ labels: type: feature-branch-deployment ``` -For using the [synchronization](#synchronization), you need to have the following pip packages installed: +For using the [synchronization](#synchronization), you need a sync tool installed. The preferred, Composer-native option is [php-sync-tool](https://github.com/konradmichalik/php-sync-tool): + +```bash +$ composer require --dev konradmichalik/php-sync-tool +``` + +It is auto-detected at `vendor/bin/sync-tool` and used automatically once installed, no `deploy.php` change needed. The legacy Python tools remain supported as a fallback for projects that have not migrated yet: ```bash $ pip3 install db-sync-tool-kmi file-sync-tool-kmi From 6065ce71e1377a3e8f36d3296feda8d070d586d0 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Tue, 8 Sep 2026 11:44:38 +0200 Subject: [PATCH 3/4] fix: identify php-sync-tool by exact binary path, not path shape --- deployer/functions.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/deployer/functions.php b/deployer/functions.php index 62ce94a..30e8fd7 100644 --- a/deployer/functions.php +++ b/deployer/functions.php @@ -155,22 +155,24 @@ function resolveSyncTool(string $legacyBinary, string $phpBinary = 'vendor/bin/s } /** - * Whether a resolved sync tool binary is php-sync-tool rather than the legacy tool: - * a path containing a slash, as opposed to a bare PATH command. + * Whether a resolved sync tool binary is php-sync-tool rather than the legacy tool. + * Compared against the known php-sync-tool path, not path-shape - a downstream + * project's legacy binary can itself be configured as an absolute or relative path, + * which would otherwise be misclassified. */ -function usingPhpSyncTool(string $resolvedBinary): bool +function usingPhpSyncTool(string $resolvedBinary, string $phpBinary = 'vendor/bin/sync-tool'): bool { - return str_contains($resolvedBinary, '/'); + return $resolvedBinary === $phpBinary; } /** * Checks whether a resolved sync tool binary is actually available locally: a path - * (php-sync-tool) via isExecutableLocally(), a bare PATH command (the legacy tool) via + * (containing a slash) via isExecutableLocally(), a bare PATH command via * commandExistLocally(). */ function syncToolAvailableLocally(string $binary): bool { - return usingPhpSyncTool($binary) + return str_contains($binary, '/') ? isExecutableLocally($binary) : commandExistLocally($binary); } From c8cc9f27473f948aaf493a61d6ac261e415f493e Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Tue, 8 Sep 2026 11:44:46 +0200 Subject: [PATCH 4/4] fix: quote sync tool binary before local shell execution --- deployer/dev/task/dump.php | 2 +- deployer/dev/task/import.php | 2 +- deployer/dev/task/sync.php | 2 +- deployer/feature/task/feature_sync.php | 2 +- deployer/functions.php | 2 +- deployer/sync/task/database_backup.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deployer/dev/task/dump.php b/deployer/dev/task/dump.php index ddcd606..914f187 100644 --- a/deployer/dev/task/dump.php +++ b/deployer/dev/task/dump.php @@ -26,6 +26,6 @@ $dumpLocationOptions = usingPhpSyncTool($dbSyncTool) ? '' : "-kd $dbDumpDir -dn $dbDumpFilename"; $dbSyncToolConfigPath = get('dev_db_sync_tool_config_path'); - runLocally("$dbSyncTool -f $dbSyncToolConfigPath/$dbSyncToolSync -y $dumpLocationOptions $additionalOptions", ['real_time_output' => true]); + runLocally(escapeshellarg($dbSyncTool) . " -f $dbSyncToolConfigPath/$dbSyncToolSync -y $dumpLocationOptions $additionalOptions", ['real_time_output' => true]); }) ->desc('Sync database with drush'); diff --git a/deployer/dev/task/import.php b/deployer/dev/task/import.php index 8f22450..8b6647b 100644 --- a/deployer/dev/task/import.php +++ b/deployer/dev/task/import.php @@ -11,6 +11,6 @@ $dbSyncTool = requireSyncTool('import'); $dbSyncToolConfigPath = get('dev_db_sync_tool_config_path'); - runLocally("$dbSyncTool -f $dbSyncToolConfigPath/$dbSyncToolSync -y -i $dbDumpDir/$dbDumpFilename.sql", ['real_time_output' => true]); + runLocally(escapeshellarg($dbSyncTool) . " -f $dbSyncToolConfigPath/$dbSyncToolSync -y -i $dbDumpDir/$dbDumpFilename.sql", ['real_time_output' => true]); }) ->desc('Sync database with drush'); diff --git a/deployer/dev/task/sync.php b/deployer/dev/task/sync.php index 28e2c4d..4730d15 100644 --- a/deployer/dev/task/sync.php +++ b/deployer/dev/task/sync.php @@ -50,7 +50,7 @@ $dbSyncTool = requireSyncTool('sync'); $dbSyncToolConfigPath = get('dev_db_sync_tool_config_path'); - runLocally("$dbSyncTool -f $dbSyncToolConfigPath/$dbSyncToolSync -y $additionalOptions", ['real_time_output' => true]); + runLocally(escapeshellarg($dbSyncTool) . " -f $dbSyncToolConfigPath/$dbSyncToolSync -y $additionalOptions", ['real_time_output' => true]); info("💽 Database from $target synced successfully"); }) ->desc('Sync database with db-sync-tool'); diff --git a/deployer/feature/task/feature_sync.php b/deployer/feature/task/feature_sync.php index e016989..396245e 100644 --- a/deployer/feature/task/feature_sync.php +++ b/deployer/feature/task/feature_sync.php @@ -133,7 +133,7 @@ function resolveDatabaseHostToIp(string $hostname): void // php-sync-tool has file sync integrated, no separate file_sync_tool call needed $withFiles = $isPhpSyncTool && false !== get('file_sync_tool') ? '--with-files' : ''; info('Synching database'); - runLocally("$dbSyncTool -f {{feature_sync_config}} --target-path {{feature_sync_target_path}} $useRsync $withFiles -y $optionalVerbose"); + runLocally(escapeshellarg($dbSyncTool) . " -f {{feature_sync_config}} --target-path {{feature_sync_target_path}} $useRsync $withFiles -y $optionalVerbose"); $synced = true; } else { debug("Skipping database sync, command \”$dbSyncTool\” not available"); diff --git a/deployer/functions.php b/deployer/functions.php index 30e8fd7..4912d76 100644 --- a/deployer/functions.php +++ b/deployer/functions.php @@ -138,7 +138,7 @@ function commandExistLocally(string $command): bool */ function isExecutableLocally(string $path): bool { - return testLocally("[ -x $path ]"); + return testLocally('[ -x ' . escapeshellarg($path) . ' ]'); } /** diff --git a/deployer/sync/task/database_backup.php b/deployer/sync/task/database_backup.php index 4b10a66..fcfb6c5 100644 --- a/deployer/sync/task/database_backup.php +++ b/deployer/sync/task/database_backup.php @@ -16,7 +16,7 @@ if (syncToolAvailableLocally($dbSyncTool)) { $useRsync = usingPhpSyncTool($dbSyncTool) ? '' : '--use-rsync'; info('Generating a database backup'); - runLocally("$dbSyncTool -f {{sync_database_backup_config}} $useRsync -y $optionalVerbose"); + runLocally(escapeshellarg($dbSyncTool) . " -f {{sync_database_backup_config}} $useRsync -y $optionalVerbose"); } else { debug("Skipping database backup, $dbSyncTool not available"); }