From edf639fea18f57b46ff74c999aa24fbcfc18a993 Mon Sep 17 00:00:00 2001 From: Danny Painter Date: Sat, 19 Sep 2026 14:51:32 +0100 Subject: [PATCH 1/5] Detect database sync failures The export/import pipeline ignored its exit status, so a failed remote export could pipe a truncated dump into wp db import and the sync would still report success. Run the pipe under pipefail, count a non-zero exit as a failed task, and skip the post-sync queries when the import didn't complete. --- tasks/database_sync.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tasks/database_sync.php b/tasks/database_sync.php index 54bf900..187151a 100644 --- a/tasks/database_sync.php +++ b/tasks/database_sync.php @@ -16,12 +16,18 @@ $command = 'ssh '.$ssh_username.'@'.$ssh_hostname.' "bash -c \"cd '.$rem_proj_loc.' && '.$remote_wp_cli.' db export --single-transaction -\"" '.$pipe.' '.$local_wp_cli.' db import -'; debug_message($command); -system($command); +// pipefail catches a failed export, not just a failed import +system('bash -c '.escapeshellarg('set -o pipefail; '.$command), $db_status); + +if ($db_status !== 0) { + task_message('Database sync failed (exit code '.$db_status.')', 'Error', 31); + $fail_count++; +} /** - * TASK: Post sync queries + * TASK: Post sync queries (skipped if the import failed) */ -if ($queries = $_ENV['DEV_POST_SYNC_QUERIES']) { +if ($db_status === 0 && ($queries = $_ENV['DEV_POST_SYNC_QUERIES'])) { $command = $local_wp_cli . ' db query "' . preg_replace('/(`|")/i', '\\\\${1}', $queries) . '"'; debug_message($command); system($command); From 913f1474a14be93ae5eadd5528a92c922d06d9f9 Mon Sep 17 00:00:00 2001 From: Danny Painter Date: Sat, 19 Sep 2026 16:29:02 +0100 Subject: [PATCH 2/5] Fail the remote WP-CLI check on unexpected output Only the literal string 'false' counted as a failure, so an ssh connection that dropped after the first check and produced no output slipped through and the sync carried on against a broken connection. --- tasks/connection_check.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tasks/connection_check.php b/tasks/connection_check.php index 623b73d..2739052 100644 --- a/tasks/connection_check.php +++ b/tasks/connection_check.php @@ -62,7 +62,8 @@ $command = 'ssh -q '.$ssh_username.'@'.$ssh_hostname.' "bash -c \"test -f '.$remote_wp_cli.' && echo true || echo false\""'; $live_server_check = exec($command); -if ($live_server_check == 'false') { +// Anything but an explicit 'true' (e.g. no output from a dropped connection) is a failure +if ($live_server_check !== 'true') { // Exit Messages task_message('Connected but cannot find remote WP-CLI', 'Error', 31, false); From dd8e1109de1521d1e083b959e1e31160257fa188 Mon Sep 17 00:00:00 2001 From: Danny Painter Date: Sat, 19 Sep 2026 16:29:02 +0100 Subject: [PATCH 3/5] Check the rsync exit status in the sync tasks A failed rsync left no trace: the task printed its progress and the run finished green. Non-zero exits now count as failed tasks, except code 24 (files vanished mid-transfer), which is routine when the live site is writing uploads during the sync. --- tasks/plugins_sync.php | 7 ++++++- tasks/uploads_sync.php | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/tasks/plugins_sync.php b/tasks/plugins_sync.php index 3833faa..bb0373a 100644 --- a/tasks/plugins_sync.php +++ b/tasks/plugins_sync.php @@ -17,7 +17,12 @@ task_message($task_name); $command = 'rsync -avhP ' . $ssh_username . '@' . $ssh_hostname . ':' . $rem_proj_loc . '/' . $plugin_dir . '/ ./' . $plugin_dir . '/'; debug_message($command); - system($command); + system($command, $rsync_status); + + if ($rsync_status !== 0 && $rsync_status !== 24) { + task_message($task_name.' failed (rsync exit code '.$rsync_status.')', 'Error', 31); + $fail_count++; + } } else { task_message($task_name.' task not ran, please install \'rsync\'', 'Error', 31); $fail_count++; diff --git a/tasks/uploads_sync.php b/tasks/uploads_sync.php index 965197b..f54a2eb 100644 --- a/tasks/uploads_sync.php +++ b/tasks/uploads_sync.php @@ -17,7 +17,13 @@ task_message($task_name); $command = 'rsync -avhP ' . $ssh_username . '@' . $ssh_hostname . ':' . $rem_proj_loc . '/' . $upload_dir . '/ ./' . $upload_dir . '/' . $excludes; debug_message($command); - system($command); + system($command, $rsync_status); + + // 24 = files vanished mid-transfer, routine on a live uploads folder + if ($rsync_status !== 0 && $rsync_status !== 24) { + task_message($task_name.' failed (rsync exit code '.$rsync_status.')', 'Error', 31); + $fail_count++; + } } else { task_message($task_name.' task not ran, please install \'rsync\'', 'Error', 31); $fail_count++; From a53f2b7d8edae51824e3d61ca51ed76269167f69 Mon Sep 17 00:00:00 2001 From: Danny Painter Date: Sat, 19 Sep 2026 16:29:29 +0100 Subject: [PATCH 4/5] Shell-escape the rsync paths and excludes A project location, uploads folder or exclude pattern containing a space or shell metacharacter split the rsync command apart. The remote and local paths and each exclude are now passed through escapeshellarg. --- tasks/plugins_sync.php | 2 +- tasks/uploads_sync.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tasks/plugins_sync.php b/tasks/plugins_sync.php index bb0373a..3b20cb8 100644 --- a/tasks/plugins_sync.php +++ b/tasks/plugins_sync.php @@ -15,7 +15,7 @@ if (`which rsync`) { task_message($task_name); - $command = 'rsync -avhP ' . $ssh_username . '@' . $ssh_hostname . ':' . $rem_proj_loc . '/' . $plugin_dir . '/ ./' . $plugin_dir . '/'; + $command = 'rsync -avhP ' . escapeshellarg($ssh_username . '@' . $ssh_hostname . ':' . $rem_proj_loc . '/' . $plugin_dir . '/') . ' ' . escapeshellarg('./' . $plugin_dir . '/'); debug_message($command); system($command, $rsync_status); diff --git a/tasks/uploads_sync.php b/tasks/uploads_sync.php index f54a2eb..5ddd9ed 100644 --- a/tasks/uploads_sync.php +++ b/tasks/uploads_sync.php @@ -9,13 +9,13 @@ if ($exclude_dirs = $_ENV['DEV_SYNC_DIR_EXCLUDES']) { $exclude_dirs = explode(',', $exclude_dirs); foreach ($exclude_dirs as $dir) { - $excludes .= ' --exclude=' . $dir; + $excludes .= ' --exclude=' . escapeshellarg($dir); } } if (`which rsync`) { task_message($task_name); - $command = 'rsync -avhP ' . $ssh_username . '@' . $ssh_hostname . ':' . $rem_proj_loc . '/' . $upload_dir . '/ ./' . $upload_dir . '/' . $excludes; + $command = 'rsync -avhP ' . escapeshellarg($ssh_username . '@' . $ssh_hostname . ':' . $rem_proj_loc . '/' . $upload_dir . '/') . ' ' . escapeshellarg('./' . $upload_dir . '/') . $excludes; debug_message($command); system($command, $rsync_status); From fc3d622d1dd602925168442b163047f075dc4266 Mon Sep 17 00:00:00 2001 From: Danny Painter Date: Sat, 19 Sep 2026 18:17:39 +0100 Subject: [PATCH 5/5] Replace backtick operators with shell_exec() PHP 8.5 deprecates the backtick operator, so the which pv / which rsync checks throw deprecation notices during a sync. Backticks are an alias for shell_exec(), so this is the same behaviour on every PHP version, just without the notice. --- tasks/database_sync.php | 2 +- tasks/plugins_sync.php | 2 +- tasks/uploads_sync.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tasks/database_sync.php b/tasks/database_sync.php index 187151a..473085e 100644 --- a/tasks/database_sync.php +++ b/tasks/database_sync.php @@ -7,7 +7,7 @@ task_message($task_name); // pv check -if (`which pv`) { +if (shell_exec('which pv')) { $pipe = '| pv |'; } else { task_message('Install the \'pv\' command to monitor import progress', 'Notice', 33, false); diff --git a/tasks/plugins_sync.php b/tasks/plugins_sync.php index 3b20cb8..71dd60d 100644 --- a/tasks/plugins_sync.php +++ b/tasks/plugins_sync.php @@ -13,7 +13,7 @@ return; } -if (`which rsync`) { +if (shell_exec('which rsync')) { task_message($task_name); $command = 'rsync -avhP ' . escapeshellarg($ssh_username . '@' . $ssh_hostname . ':' . $rem_proj_loc . '/' . $plugin_dir . '/') . ' ' . escapeshellarg('./' . $plugin_dir . '/'); debug_message($command); diff --git a/tasks/uploads_sync.php b/tasks/uploads_sync.php index 5ddd9ed..b1936fc 100644 --- a/tasks/uploads_sync.php +++ b/tasks/uploads_sync.php @@ -13,7 +13,7 @@ } } -if (`which rsync`) { +if (shell_exec('which rsync')) { task_message($task_name); $command = 'rsync -avhP ' . escapeshellarg($ssh_username . '@' . $ssh_hostname . ':' . $rem_proj_loc . '/' . $upload_dir . '/') . ' ' . escapeshellarg('./' . $upload_dir . '/') . $excludes; debug_message($command);