From bfe5803a303cc7a0e07e77e9855b0699ff5780f3 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 10 Jul 2023 09:25:11 +0200 Subject: [PATCH 01/36] add check if chown is available --- recipes/common.php | 2 +- src/set.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/recipes/common.php b/recipes/common.php index 1704538..31c848d 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -105,7 +105,7 @@ * Does not support writable_mode configuration - always uses this */ task('deploy:writable', function () { - if (has('http_user')) { + if (has('http_user') && get('writable_mode') === 'chown') { run("cd {{release_or_current_path}} && chown -R {{http_user}} ."); } // set all directories to 755 diff --git a/src/set.php b/src/set.php index d8ab1fe..e295e11 100644 --- a/src/set.php +++ b/src/set.php @@ -8,7 +8,7 @@ namespace Deployer; use function Gaambo\DeployerWordpress\Utils\Composer\installComposer; -use function \Gaambo\DeployerWordpress\Utils\WPCLI\installWPCLI; +use function Gaambo\DeployerWordpress\Utils\WPCLI\installWPCLI; require_once 'utils/wp-cli.php'; @@ -162,3 +162,5 @@ set('release_name', function () { return date('YmdHis'); // you could also use the composer.json version here }); + +set('writable_mode', 'chown'); From 291f1f6a2566bf6fafa3f57fed9923fc1b3520f3 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Thu, 6 Mar 2025 17:01:29 +0100 Subject: [PATCH 02/36] [WIP] refactor to packages --- recipes/common.php | 11 +-- src/set.php | 2 + src/tasks/database.php | 16 ++++ src/tasks/files.php | 13 +-- src/tasks/mu-plugins.php | 8 +- src/tasks/packages.php | 182 +++++++++++++++++++++++++++++++++++++++ src/tasks/plugins.php | 4 +- src/tasks/themes.php | 4 +- src/tasks/uploads.php | 16 +++- src/tasks/wp.php | 15 +++- 10 files changed, 246 insertions(+), 25 deletions(-) create mode 100644 src/tasks/packages.php diff --git a/recipes/common.php b/recipes/common.php index 31c848d..483274e 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -28,6 +28,7 @@ require_once 'tasks/database.php'; require_once 'tasks/files.php'; require_once 'tasks/mu-plugins.php'; +require_once 'tasks/packages.php'; require_once 'tasks/plugins.php'; require_once 'tasks/themes.php'; require_once 'tasks/uploads.php'; @@ -50,12 +51,12 @@ 'deploy:release' ])->desc('Prepares a new release'); -// Build theme assets via npm locally +// Build package assets via npm locally task('deploy:build_assets', function () { on(getLocalhost(), function () { - if (has('theme/name')) { - invoke('theme:assets:vendors'); - invoke('theme:assets:build'); + if (has('packages')) { + invoke('packages:assets:vendors'); + invoke('packages:assets:build'); } }); })->once(); @@ -63,7 +64,7 @@ // Overwrite deployment with rsync (instead of git) Deployer::get()->tasks->remove('deploy:check_remote'); Deployer::get()->tasks->remove('deploy:update_code'); -// Push all files (incl 'wp:push', 'uploads:push', 'plugins:push', 'mu-plugins:push', 'themes:push') +// Push all files (incl 'wp:push', 'uploads:push', 'plugins:push', 'mu-plugins:push', 'themes:push', 'packages:push') task('deploy:update_code', ['files:push']) ->desc('Pushes local code to the remote hosts'); diff --git a/src/set.php b/src/set.php index e295e11..f506dc3 100644 --- a/src/set.php +++ b/src/set.php @@ -117,6 +117,8 @@ set('themes/dir', 'wp-content/themes'); // relative to document root set('themes/filter', []); // rsync filter syntax set('theme/build_script', 'build'); // custom theme npm build script +set('languages/dir', 'wp-content/languages'); // relative to document root +set('languages/filter', []); // rsync filter syntax // options for zipping files for backups - passed to zip shell command set('zip_options', '-x "_backup_*.zip" -x **/node_modules/**\* -x **/vendor/**\*'); diff --git a/src/tasks/database.php b/src/tasks/database.php index 2caf2d3..763be71 100644 --- a/src/tasks/database.php +++ b/src/tasks/database.php @@ -77,6 +77,14 @@ $localUrl = getLocalhost()->get('public_url'); run("cd {{release_or_current_path}} && {{bin/wp}} db import {{dump_filepath}}"); run("cd {{release_or_current_path}} && {{bin/wp}} search-replace $localUrl {{public_url}}"); + + // If the local uploads directory is different than the remote one + // replace all references to the local uploads directory with the remote one + $localUploadsDir = getLocalhost()->get('uploads/dir'); + if ($localUploadsDir !== get('uploads/dir')) { + run("cd {{release_or_current_path}} && {{bin/wp}} search-replace $localUploadsDir {{uploads/dir}}"); + } + run('rm -f {{dump_filepath}}'); })->desc('Imports Database on remote host'); @@ -94,6 +102,14 @@ $localDumpPath = getLocalhost()->get('dump_path'); runLocally("$localWp db import $localDumpPath/{{dump_file}}"); runLocally("$localWp search-replace {{public_url}} $localUrl"); + + // If the local uploads directory is different than the remote one + // replace all references to the remotes uploads directory with the local one + $localUploadsDir = getLocalhost()->get('uploads/dir'); + if ($localUploadsDir !== get('uploads/dir')) { + run("cd {{release_or_current_path}} && {{bin/wp}} search-replace {{uploads/dir}} $localUploadsDir"); + } + runLocally("rm -f $localDumpPath/{{dump_file}}"); })->desc('Imports Database on local host'); diff --git a/src/tasks/files.php b/src/tasks/files.php index 33144ab..7ffc2f6 100644 --- a/src/tasks/files.php +++ b/src/tasks/files.php @@ -10,19 +10,20 @@ use function Deployer\task; require_once 'mu-plugins.php'; +require_once 'packages.php'; require_once 'plugins.php'; require_once 'themes.php'; require_once 'uploads.php'; require_once 'wp.php'; // Pushes all files from local to remote host -// Runs wp:push, uploads:push, plugins:push, mu-plugins:push, themes:push in series +// Runs wp:push, uploads:push, plugins:push, mu-plugins:push, themes:push, packages:push in series // see tasks definitions for details and required variables -task('files:push', ['wp:push', 'uploads:push', 'plugins:push', 'mu-plugins:push', 'themes:push']) - ->desc("Pushes all files from local to remote host (combines `wp:push`, `uploads:push`, `plugins:push`, `mu-plugins:push`, `themes:push`)"); +task('files:push', ['wp:push', 'uploads:push', 'plugins:push', 'mu-plugins:push', 'themes:push', 'packages:push']) + ->desc("Pushes all files from local to remote host (combines `wp:push`, `uploads:push`, `plugins:push`, `mu-plugins:push`, `themes:push`, `packages:push`)"); // Pulls all files from remote to local host -// Runs wp:pull, uploads:pull, plugins:pull, mu-plugins:pull, themes:pull in series +// Runs wp:pull, uploads:pull, plugins:pull, mu-plugins:pull, themes:pull, packages:pull in series // see tasks definitions for details and required variables -task('files:pull', ['wp:pull', 'uploads:pull', 'plugins:pull', 'mu-plugins:pull', 'themes:pull']) - ->desc("Pulls all files from remote to local host (combines `wp:pull`, `uploads:pull`, `plugins:pull`, `mu-plugins:pull`, `themes:pull`)"); +task('files:pull', ['wp:pull', 'uploads:pull', 'plugins:pull', 'mu-plugins:pull', 'themes:pull', 'packages:pull']) + ->desc("Pulls all files from remote to local host (combines `wp:pull`, `uploads:pull`, `plugins:pull`, `mu-plugins:pull`, `themes:pull`, `packages:pull`)"); diff --git a/src/tasks/mu-plugins.php b/src/tasks/mu-plugins.php index 94cc1fc..a042c86 100644 --- a/src/tasks/mu-plugins.php +++ b/src/tasks/mu-plugins.php @@ -53,7 +53,7 @@ $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("mu-plugins/filter"), ]); - pushFiles('{{mu-plugins/dir}}', '{{mu-plugins/dir}}', $rsyncOptions); + pushFiles(getLocalhost()->get('mu-plugins/dir'), '{{mu-plugins/dir}}', $rsyncOptions); })->desc('Push mu-plugins from local to remote'); /** @@ -67,7 +67,7 @@ $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("mu-plugins/filter"), ]); - pullFiles('{{mu-plugins/dir}}', '{{mu-plugins/dir}}', $rsyncOptions); + pullFiles('{{mu-plugins/dir}}', getLocalhost()->get('mu-plugins/dir'), $rsyncOptions); })->desc('Pull mu-plugins from remote to local'); /** @@ -102,10 +102,10 @@ * - backup_path (on localhost): Path to directory in which to store all backups */ task('mu-plugins:backup:local', function () { - $localPath = getLocalhost()->get('current_path'); + $localPath = getLocalhost()->get('mu-plugins/dir'); $localBackupPath = getLocalhost()->get('backup_path'); $backupFile = zipFiles( - "$localPath/{{mu-plugins/dir}}/", + "$localPath/", $localBackupPath, 'backup_mu-plugins' ); diff --git a/src/tasks/packages.php b/src/tasks/packages.php new file mode 100644 index 0000000..7928994 --- /dev/null +++ b/src/tasks/packages.php @@ -0,0 +1,182 @@ +desc("Install package assets vendors/dependencies (npm)"); + +/** + * Run package assets (npm) build script + * + * Needs the following config per package: + * - 'path' (string): Path of package relative to release_path/current_path + * - (optional) assets:build_script: NPM script to be run (must be defined in package.json, has a default of "build") + */ +task('packages:assets:build', function () { + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + if (empty($package['assets'])) { + continue; + } + \Gaambo\DeployerWordpress\Utils\Npm\runScript( + "{{release_or_current_path}}/$packagePath", + $package['assets:build_script'] ?? 'build' + ); + } +})->desc("Run package assets (npm) build script"); + +/** + * Install package assets vendors/dependencies (npm) and run build script + * Runs package:assets:vendors and package:assets:build tasks in series + */ +task('packages:assets', ['packages:assets:vendors', 'packages:assets:build']) + ->desc("A combined task to prepare the packages assets - combines `packages:assets` and `packages:vendors`"); + +/** + * Install packages vendors (composer) + * + * Needs the following config per package: + * - 'path' (string): Path of package relative to release_path/current_path + */ +task('packages:vendors', function () { + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + \Gaambo\DeployerWordpress\Utils\Composer\runDefault( + "{{release_or_current_path}}/$packagePath" + ); + } +})->desc("Install packages vendors (composer)"); + +/** + * Install packages vendors (composer + npm) and build assets (npm) + * + * Runs packages:assets and packages:vendors tasks in series + * See tasks definitions for required variables + */ +task('packages', ['packages:assets', 'packages:vendors']) + ->desc("A combined task to prepare the packages - combines `packages:assets` and `packages:vendors`"); + +/** + * Push packages from local to remote + * + * Needs the following config per package: + * - 'path' (string): Path of package relative to release_path/current_path + * - 'remote:path' (string): Path of package on remote host + * - (optional) 'rsync:filter' (array): rsync filter syntax array of files to push + */ +task('packages:push', function () { + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + $remotePath = $package['remote:path']; + $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ + 'filter' => $package['rsync:filter'] ?? [], + ]); + run("mkdir -p {{release_or_current_path}}/$remotePath"); + pushFiles($packagePath, $remotePath, $rsyncOptions); + } +})->desc('Push packages from local to remote'); + +/** + * Pull packages from remote to local + * + * Needs the following config per package: + * - 'path' (string): Path of package relative to release_path/current_path + * - 'remote:path' (string): Path of package on remote host + * - (optional) 'rsync:filter' (array): rsync filter syntax array of files to push + */ +task('packages:pull', function () { + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + $remotePath = $package['remote:path']; + $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ + 'filter' => $package['rsync:filter'] ?? [], + ]); + pullFiles($remotePath, $packagePath, $rsyncOptions); + } +})->desc('Pull packages from remote to local'); + +/** + * Syncs packages between remote and local + * + * Runs packages:push and packages:pull tasks in series + * See tasks definitions for required variables + */ +task("packages:sync", ["packages:push", "packages:pull"])->desc("Sync packages"); + +/** + * Backup packages on remote host and downloads zip to local backup path + * + * Needs the following config per package: + * - 'remote:path' (string): Path of package on remote host + * - backup_path (on remote host): Path to directory in which to store all backups + * - backup_path (on localhost): Path to directory in which to store all backups + */ +task('packages:backup:remote', function () { + foreach (get('packages', []) as $package) { + $remotePath = $package['remote:path']; + $backupFile = zipFiles( + "{{release_or_current_path}}/$remotePath/", + '{{backup_path}}', + 'backup_packages' + ); + $localBackupPath = getLocalhost()->get('backup_path'); + download($backupFile, "$localBackupPath/"); + } +})->desc('Backup packages on remote host and download zip'); + +/** + * Backup packages on local host + * + * Needs the following config per package: + * - 'remote:path' (string): Path of package on remote host + * - backup_path (on remote host): Path to directory in which to store all backups + * - backup_path (on localhost): Path to directory in which to store all backups + */ +task('packages:backup:local', function () { + $localPath = getLocalhost()->get('current_path'); + $localBackupPath = getLocalhost()->get('backup_path'); + + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + $backupFile = zipFiles( + "{{release_or_current_path}}/$packagePath/", + $localBackupPath, + 'backup_packages' + ); + } +})->once()->desc('Backup local packages as zip'); diff --git a/src/tasks/plugins.php b/src/tasks/plugins.php index 472595e..69ea33f 100644 --- a/src/tasks/plugins.php +++ b/src/tasks/plugins.php @@ -29,7 +29,7 @@ $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("plugins/filter"), ]); - pushFiles('{{plugins/dir}}', '{{plugins/dir}}', $rsyncOptions); + pushFiles(getLocalhost()->get('plugins/dir'), '{{plugins/dir}}', $rsyncOptions); })->desc('Push plugins from local to remote'); /** @@ -43,7 +43,7 @@ $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("plugins/filter"), ]); - pullFiles('{{plugins/dir}}', '{{plugins/dir}}', $rsyncOptions); + pullFiles('{{plugins/dir}}', getLocalhost()->get('plugins/dir'), $rsyncOptions); })->desc('Pull plugins from remote to local'); /** diff --git a/src/tasks/themes.php b/src/tasks/themes.php index 563b5c3..b6a4c70 100644 --- a/src/tasks/themes.php +++ b/src/tasks/themes.php @@ -85,7 +85,7 @@ $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("themes/filter"), ]); - pushFiles('{{themes/dir}}', '{{themes/dir}}', $rsyncOptions); + pushFiles(getLocalhost()->get('themes/dir'), '{{themes/dir}}', $rsyncOptions); })->desc('Push themes from local to remote'); /** @@ -99,7 +99,7 @@ $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("themes/filter"), ]); - pullFiles('{{themes/dir}}', '{{themes/dir}}', $rsyncOptions); + pullFiles('{{themes/dir}}', getLocalhost()->get('themes/dir'), $rsyncOptions); })->desc('Pull themes from remote to local'); /** diff --git a/src/tasks/uploads.php b/src/tasks/uploads.php index d8c69e0..aedd0d0 100644 --- a/src/tasks/uploads.php +++ b/src/tasks/uploads.php @@ -15,6 +15,7 @@ use function Deployer\task; use function Deployer\upload; use function \Gaambo\DeployerWordpress\Utils\Files\zipFiles; + use function Gaambo\DeployerWordpress\Utils\Localhost\getLocalhost; /** @@ -25,11 +26,17 @@ * - uploads/path: Path to directory which contains the uploads directory on remote (eg shared directory, has a default) */ task('uploads:push', function () { - $localPath = getLocalhost()->get('current_path'); + $localUploadsPath = getLocalhost()->get('uploads/path'); + $localUploadsDir = getLocalhost()->get('uploads/dir'); $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("uploads/filter"), ]); - upload("$localPath/{{uploads/dir}}/", '{{uploads/path}}/{{uploads/dir}}/', ['options' => $rsyncOptions]); + + upload( + "$localUploadsPath/$localUploadsDir/", + '{{uploads/path}}/{{uploads/dir}}/', + ['options' => $rsyncOptions] + ); })->desc('Push uploads from local to remote'); /** @@ -40,11 +47,12 @@ * - uploads/path: Path to directory which contains the uploads directory on remote (eg shared directory, has a default) */ task('uploads:pull', function () { - $localPath = getLocalhost()->get('current_path'); + $localUploadsPath = getLocalhost()->get('uploads/path'); + $localUploadsDir = getLocalhost()->get('uploads/dir'); $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("uploads/filter"), ]); - download('{{uploads/path}}/{{uploads/dir}}/', "$localPath/{{uploads/dir}}/", ['options' => $rsyncOptions]); + download("{{uploads/path}}/{{uploads/dir}}/", "$localUploadsPath/$localUploadsDir/", ['options' => $rsyncOptions]); })->desc('Pull uploads from remote to local'); /** diff --git a/src/tasks/wp.php b/src/tasks/wp.php index 32fbba0..73bb0bf 100644 --- a/src/tasks/wp.php +++ b/src/tasks/wp.php @@ -8,6 +8,7 @@ use function Deployer\get; use function Deployer\task; +use function Gaambo\DeployerWordpress\Utils\Localhost\getLocalhost; use function Gaambo\DeployerWordpress\Utils\WPCLI\installWPCLI; use function Gaambo\DeployerWordpress\Utils\WPCLI\runCommand; @@ -35,11 +36,16 @@ * - wp/dir: Path of WordPress directory relative to release_path/current_path */ task('wp:push', function () { + $localWpDir = getLocalhost()->get('wp/dir'); $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("wp/filter"), 'flags' => 'rz', ]); - \Gaambo\DeployerWordpress\Utils\Files\pushFiles('{{wp/dir}}', '{{wp/dir}}', $rsyncOptions); + \Gaambo\DeployerWordpress\Utils\Files\pushFiles( + $localWpDir, + '{{wp/dir}}', + $rsyncOptions + ); })->desc('Push WordPress core files from local to remote'); /** @@ -50,11 +56,16 @@ * - wp/dir: Path of WordPress directory relative to release_path/current_path */ task('wp:pull', function () { + $localWpDir = getLocalhost()->get('wp/dir'); $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ 'filter' => get("wp/filter"), 'flags' => 'rz', ]); - \Gaambo\DeployerWordpress\Utils\Files\pullFiles('{{wp/dir}}', '{{wp/dir}}', $rsyncOptions); + \Gaambo\DeployerWordpress\Utils\Files\pullFiles( + '{{wp/dir}}', + $localWpDir, + $rsyncOptions + ); })->desc('Pull WordPress core files from remote to local'); /** From 7462faca3cb17ef3523cda3b6dbdcd479187695d Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Fri, 7 Mar 2025 16:16:58 +0100 Subject: [PATCH 03/36] add languages tasks --- src/set.php | 5 ++- src/tasks/languages.php | 87 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 src/tasks/languages.php diff --git a/src/set.php b/src/set.php index f506dc3..7dbb5d5 100644 --- a/src/set.php +++ b/src/set.php @@ -76,15 +76,16 @@ // set all wp-config files to 600 - which means plugins/wordpress can modify it // alternative set it to 400 to disallow edits via wordpress set('wp/configFiles/permissions', '600'); -set('wp/filter', [ // contains all wordpress core files excluding uploads, themes, plugins, mu-plugins +set('wp/filter', [ // contains all wordpress core files excluding uploads, themes, plugins, mu-plugins, languages '+ /wp-content/', '- /wp-content/mu-plugins/*', '- /wp-content/plugins/*', '- /wp-content/themes/*', '- /wp-content/uploads/*', + '- /wp-content/languages/*', '- /wp-content/upgrade', '- /wp-content/cache', - '+ /wp-content/**', // all other files in wp-content eg. languages + '+ /wp-content/**', // all other files in wp-content '+ /wp-admin/', '+ /wp-admin/**', '+ /wp-includes/', diff --git a/src/tasks/languages.php b/src/tasks/languages.php new file mode 100644 index 0000000..294a668 --- /dev/null +++ b/src/tasks/languages.php @@ -0,0 +1,87 @@ + get("languages/filter"), + ]); + pushFiles(getLocalhost()->get('languages/dir'), '{{languages/dir}}', $rsyncOptions); +})->desc('Push languages from local to remote'); + +/** + * Pull languages from remote to local + * Needs the following variables: + * - languages/filter: rsync filter syntax array of files to pull (has a default) + * - languages/dir: Path of languages directory relative to release_path/current_path + * - deploy_path or release_path: to build remote path + */ +task('languages:pull', function () { + $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ + 'filter' => get("languages/filter"), + ]); + pullFiles('{{languages/dir}}', getLocalhost()->get('languages/dir'), $rsyncOptions); +})->desc('Pull languages from remote to local'); + +/** + * Syncs languages between remote and local + * Runs languages:push and languages:pull tasks in series + * See tasks definitions for required variables + */ +task("languages:sync", ["languages:push", "languages:pull"])->desc("Sync languages"); + +/** + * Backup languages on remote host and downloads zip to local backup path + * Needs the following variables: + * - languages/dir: Path of languages directory relative to release_path/current_path + * - backup_path (on remote host): Path to directory in which to store all backups + * - backup_path (on localhost): Path to directory in which to store all backups + */ +task('languages:backup:remote', function () { + $backupFile = zipFiles( + "{{release_or_current_path}}/{{languages/dir}}/", + '{{backup_path}}', + 'backup_languages' + ); + $localBackupPath = getLocalhost()->get('backup_path'); + download($backupFile, "$localBackupPath/"); +})->desc('Backup languages on remote host and download zip'); + +/** + * Backup languages on localhost + * Needs the following variables: + * - languages/dir: Path of languages directory relative to release_path/current_path + * - backup_path (on localhost): Path to directory in which to store all backups + */ +task('languages:backup:local', function () { + $localPath = getLocalhost()->get('current_path'); + $localBackupPath = getLocalhost()->get('backup_path'); + $backupFile = zipFiles( + "$localPath/{{languages/dir}}/", + $localBackupPath, + 'backup_languages' + ); +})->once()->desc('Backup local languages as zip'); From 6dd39779675663b63c9ac42b87b6b7a56f26121b Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Fri, 7 Mar 2025 16:17:37 +0100 Subject: [PATCH 04/36] add new examples for simple and bedrock, remove advanced recipe --- examples/advanced/deploy.php | 38 ----------------- examples/advanced/deploy.yml | 13 ------ examples/bedrock/deploy.php | 55 ++++++++++++++++++++++++ examples/bedrock/deploy.yml | 18 ++++++++ examples/simple/deploy.php | 33 ++++++++++----- recipes/advanced.php | 26 ------------ recipes/bedrock.php | 82 ++++++++++++++++++++++++++++++++++++ recipes/common.php | 1 + recipes/simple.php | 3 ++ 9 files changed, 181 insertions(+), 88 deletions(-) delete mode 100644 examples/advanced/deploy.php delete mode 100644 examples/advanced/deploy.yml create mode 100644 examples/bedrock/deploy.php create mode 100644 examples/bedrock/deploy.yml delete mode 100644 recipes/advanced.php create mode 100644 recipes/bedrock.php diff --git a/examples/advanced/deploy.php b/examples/advanced/deploy.php deleted file mode 100644 index d54df08..0000000 --- a/examples/advanced/deploy.php +++ /dev/null @@ -1,38 +0,0 @@ -set('public_url', "{{local_url}}") - ->set('deploy_path', __DIR__) - ->set('release_path', __DIR__ . '/public') - // set current_path to hardcoded release_path on local so release_or_current_path works; {{release_path}} does not work here? - ->set('current_path', function () { - return getLocalhost()->get('release_path'); - }) - ->set('dump_path', __DIR__ . '/data/db_dumps') - ->set('backup_path', __DIR__ . '/data/backups'); - -/** - * Example Deployment Configuration: - */ -// only push WordPress core files, themes, mu-plugins, plugins (not uploads, ) -// task('deploy:update_code', ['wp:push', 'plugins:push', 'mu-plugins:push', 'themes:push']) -// ->desc("Pushes updated code to target host"); - -// // install theme composer vendors composer on server -// after('deploy:update_code', 'theme:vendors'); - -// // install mu-plugin vendors after deploying (on remote host) -// after('deploy:update_code', 'mu-plugin:vendors'); diff --git a/examples/advanced/deploy.yml b/examples/advanced/deploy.yml deleted file mode 100644 index a91f7be..0000000 --- a/examples/advanced/deploy.yml +++ /dev/null @@ -1,13 +0,0 @@ -config: - theme/name: custom-theme - mu-plugin/name: core-functionality - local_url: http://test.local -hosts: - prod: - labels: - stage: production - hostname: test.dev - public_url: https://test.dev - deploy_path: "~" - dump_path: ~/data/dumps - backup_path: ~/data/backups diff --git a/examples/bedrock/deploy.php b/examples/bedrock/deploy.php new file mode 100644 index 0000000..96659a1 --- /dev/null +++ b/examples/bedrock/deploy.php @@ -0,0 +1,55 @@ +set('public_url', "{{local_url}}") + ->set('deploy_path', __DIR__) + // Root project directory, for app:push to work. + ->set('release_path', __DIR__) + // set current_path to hardcoded release_path on local so release_or_current_path works; {{release_path}} does not work here? + ->set('current_path', function () { + return getLocalhost()->get('release_path'); + }) + // Bedrock dirs + ->set('uploads/dir', 'web/app/uploads') + ->set('mu-plugins/dir', 'web/app/mu-plugins') + ->set('themes/dir', 'web/app/themes') + ->set('plugins/dir', 'web/app/plugins') + ->set('wp/dir', 'web/wp') + ->set('dump_path', __DIR__ . '/data/db_dumps') + ->set('backup_path', __DIR__ . '/data/backups'); + +set('packages', [ + 'theme' => [ + 'path' => '{{themes/dir}}/custom-theme', + 'remote:path' => '{{themes/dir}}/custom-theme', + 'assets' => true, + 'assets:build_script' => 'build' + ], + 'core-functionality' => [ + 'path' => '{{mu-plugins/dir}}/core-functionality', + 'remote:path' => '{{mu-plugins/dir}}/core-functionality' + ], +]); + +// Build package assets via npm locally +task('deploy:build_assets', function () { + on(getLocalhost(), function () { + if (has('packages')) { + // Do not install vendors on each deployment. + // invoke('packages:assets:vendors'); + invoke('packages:assets:build'); + } + }); +})->once(); diff --git a/examples/bedrock/deploy.yml b/examples/bedrock/deploy.yml new file mode 100644 index 0000000..4852590 --- /dev/null +++ b/examples/bedrock/deploy.yml @@ -0,0 +1,18 @@ +config: + local_url: http://test.local +hosts: + prod: + labels: + stage: production + hostname: test.dev + public_url: https://test.dev + deploy_path: "~" + release_path: "{{deploy_path}}/public_html" # fixed directory, no symlinks + # Bedrock dirs + uploads/dir: web/app/uploads + mu-plugins/dir: web/app/mu-plugins + themes/dir: web/app/themes + plugins/dir: web/app/plugins + wp/dir: web/wp + dump_path: ~/data/dumps + backup_path: ~/data/backups diff --git a/examples/simple/deploy.php b/examples/simple/deploy.php index 13bb241..6fd9715 100644 --- a/examples/simple/deploy.php +++ b/examples/simple/deploy.php @@ -3,7 +3,6 @@ require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/gaambo/deployer-wordpress/recipes/simple.php'; -use function Deployer\after; use function Deployer\import; use function Deployer\localhost; use function Deployer\task; @@ -24,14 +23,26 @@ ->set('dump_path', __DIR__ . '/data/db_dumps') ->set('backup_path', __DIR__ . '/data/backups'); -/** - * Example Deployment Configuration: - */ -// only push themes and mu-plugins -// task('deploy:update_code', ['themes:push', 'mu-plugins:push']); +set('packages', [ + 'theme' => [ + 'path' => '{{themes/dir}}/custom-theme', + 'remote:path' => '{{themes/dir}}/custom-theme', + 'assets' => true, + 'assets:build_script' => 'build' + ], + 'core-functionality' => [ + 'path' => '{{mu-plugins/dir}}/core-functionality', + 'remote:path' => '{{mu-plugins/dir}}/core-functionality' + ], +]); -// // install theme composer vendors composer on server -// after('deploy:update_code', 'theme:vendors'); - -// // install mu-plugin vendors after deploying (on remote host) -// after('deploy:update_code', 'mu-plugin:vendors'); +// Build package assets via npm locally +task('deploy:build_assets', function () { + on(getLocalhost(), function () { + if (has('packages')) { + // Do not install vendors on each deployment. + // invoke('packages:assets:vendors'); + invoke('packages:assets:build'); + } + }); +})->once(); diff --git a/recipes/advanced.php b/recipes/advanced.php deleted file mode 100644 index 41d64be..0000000 --- a/recipes/advanced.php +++ /dev/null @@ -1,26 +0,0 @@ -desc('Deploy WordPress Site'); - -add('recipes', ['base']); diff --git a/recipes/bedrock.php b/recipes/bedrock.php new file mode 100644 index 0000000..0ab70e4 --- /dev/null +++ b/recipes/bedrock.php @@ -0,0 +1,82 @@ + $rsyncOptions]); + upload([ + // Keep prod .htaccess with webp redirects + redirection redirects + wprocket + // 'web/.htaccess', + 'web/index.php', + 'web/wp-config.php', + ], "{{release_or_current_path}}/web", ['options' => $rsyncOptions]); + upload([ + 'web/app/mu-plugins/bedrock-autoloader.php', + ], "{{release_or_current_path}}/{{mu-plugins/dir}}", ['options' => $rsyncOptions]); +}); + +task('deploy:update_code', [ + 'app:push', + 'packages:push', +])->desc('Pushes local bedrock app and packages to the remote hosts'); + +// install vendors after deploying (on remote host) +after( + 'deploy:update_code', + function () { + \Gaambo\DeployerWordpress\Utils\Composer\runDefault( + '{{release_or_current_path}}' + ); + } +); + +task('deploy', [ + 'deploy:prepare', + 'deploy:build_assets', + 'deploy:update_code', + 'deploy:publish' +])->desc('Deploy WordPress Site'); diff --git a/recipes/common.php b/recipes/common.php index 483274e..d3bddc1 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -97,6 +97,7 @@ task('cache:clear', function () { // TODO: overwrite, maybe clear cache via wpcli // run("cd {{release_or_current_path}} && {{bin/wp}} rocket clean --confirm"); + // run("cd {{release_or_current_path}} && {{bin/wp}} cache flush"); }); /** diff --git a/recipes/simple.php b/recipes/simple.php index fff30c9..95c353c 100644 --- a/recipes/simple.php +++ b/recipes/simple.php @@ -36,6 +36,9 @@ set('shared_files', []); set('shared_dirs', []); +task('deploy:update_code', ['packages:push']) + ->desc('Pushes local packages to the remote hosts'); + task('deploy', [ 'deploy:prepare', 'deploy:build_assets', From d5fefb6a98c077cdaec96128ee71c3a2c34bc3f0 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Fri, 7 Mar 2025 16:30:38 +0100 Subject: [PATCH 05/36] add phpcs and format all files --- composer.json | 8 +++++++- composer.lock | 8 ++++---- examples/bedrock/deploy.php | 3 ++- examples/simple/deploy.php | 3 ++- phpcs.xml | 17 +++++++++++++++++ recipes/bedrock.php | 14 +++++++------- src/set.php | 6 ++++-- src/tasks/files.php | 4 +++- src/tasks/languages.php | 6 +++--- src/tasks/mu-plugins.php | 6 +++--- src/tasks/packages.php | 6 +++--- src/tasks/plugins.php | 6 +++--- src/tasks/themes.php | 11 +++++++---- src/tasks/uploads.php | 10 +++++----- src/tasks/wp.php | 1 + src/utils/files.php | 6 +++--- 16 files changed, 74 insertions(+), 41 deletions(-) create mode 100644 phpcs.xml diff --git a/composer.json b/composer.json index f2ab7d6..fd48983 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,9 @@ "email": "fabian@fabiantodt.at" } ], - "require-dev": {}, + "require-dev": { + "squizlabs/php_codesniffer": "^3.7" + }, "require": { "php": "^8.0|^7.3", "deployer/deployer": "^7.3" @@ -20,5 +22,9 @@ "files": [ "autoload.php" ] + }, + "scripts": { + "phpcs": "phpcs", + "phpcs:fix": "phpcbf" } } diff --git a/composer.lock b/composer.lock index d566f23..0283b91 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7d62bb63546044425055905e73b0ace8", + "content-hash": "c1d3afa8b7c8686c0065e6724d8ab35b", "packages": [ { "name": "deployer/deployer", @@ -53,12 +53,12 @@ "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^8.0|^7.3" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/examples/bedrock/deploy.php b/examples/bedrock/deploy.php index 96659a1..3e28f81 100644 --- a/examples/bedrock/deploy.php +++ b/examples/bedrock/deploy.php @@ -17,7 +17,8 @@ ->set('deploy_path', __DIR__) // Root project directory, for app:push to work. ->set('release_path', __DIR__) - // set current_path to hardcoded release_path on local so release_or_current_path works; {{release_path}} does not work here? + // set current_path to hardcoded release_path on local so release_or_current_path works; + // {{release_path}} does not work here? ->set('current_path', function () { return getLocalhost()->get('release_path'); }) diff --git a/examples/simple/deploy.php b/examples/simple/deploy.php index 6fd9715..dfae362 100644 --- a/examples/simple/deploy.php +++ b/examples/simple/deploy.php @@ -16,7 +16,8 @@ ->set('public_url', "{{local_url}}") ->set('deploy_path', __DIR__) ->set('release_path', __DIR__ . '/public') - // set current_path to hardcoded release_path on local so release_or_current_path works; {{release_path}} does not work here? + // set current_path to hardcoded release_path on local so release_or_current_path works; + // {{release_path}} does not work here? ->set('current_path', function () { return getLocalhost()->get('release_path'); }) diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..62b18fc --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,17 @@ + + + + + + + + src + recipes + examples + + + vendor/* + + + + \ No newline at end of file diff --git a/recipes/bedrock.php b/recipes/bedrock.php index 0ab70e4..ca247a0 100644 --- a/recipes/bedrock.php +++ b/recipes/bedrock.php @@ -37,7 +37,7 @@ set('shared_dirs', []); // Tasks -task('app:push',function() { +task('app:push', function () { $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray(); run("mkdir -p {{release_or_current_path}}"); upload([ @@ -66,12 +66,12 @@ // install vendors after deploying (on remote host) after( - 'deploy:update_code', - function () { - \Gaambo\DeployerWordpress\Utils\Composer\runDefault( - '{{release_or_current_path}}' - ); - } + 'deploy:update_code', + function () { + \Gaambo\DeployerWordpress\Utils\Composer\runDefault( + '{{release_or_current_path}}' + ); + } ); task('deploy', [ diff --git a/src/set.php b/src/set.php index 7dbb5d5..a42aea7 100644 --- a/src/set.php +++ b/src/set.php @@ -69,7 +69,8 @@ // PATHS & FILES CONFIGURATION // if you want to further define options for rsyncing files -// just look at the source in `files.php` and use the Rsync\buildConfig, Files\pushFiles and Files\pullFiles utils methods +// just look at the source in `files.php` +// and use the Rsync\buildConfig, Files\pushFiles and Files\pullFiles utils methods set('wp/dir', ''); // relative to document root // config files which should be protected - add to shared_files as well set('wp/configFiles', ['wp-config.php', 'wp-config-local.php']); @@ -139,7 +140,8 @@ 'include-file' => false, 'filter' => [], 'filter-file' => false, - // Allows specifying (=excluding/including/filtering) files to sync per directory in a `.deployfilter` file - See README directory for examples + // Allows specifying (=excluding/including/filtering) files to sync per directory in a `.deployfilter` file + // See README directory for examples 'filter-perdir' => '.deployfilter', 'flags' => 'rz', // Recursive, with compress 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterwards diff --git a/src/tasks/files.php b/src/tasks/files.php index 7ffc2f6..ab0d15a 100644 --- a/src/tasks/files.php +++ b/src/tasks/files.php @@ -20,10 +20,12 @@ // Runs wp:push, uploads:push, plugins:push, mu-plugins:push, themes:push, packages:push in series // see tasks definitions for details and required variables task('files:push', ['wp:push', 'uploads:push', 'plugins:push', 'mu-plugins:push', 'themes:push', 'packages:push']) - ->desc("Pushes all files from local to remote host (combines `wp:push`, `uploads:push`, `plugins:push`, `mu-plugins:push`, `themes:push`, `packages:push`)"); + // phpcs:ignore Generic.Files.LineLength.TooLong + ->desc("Pushes all files from local to remote host (combines push for wp, uploads, plugins, mu-plugins, themes, packages`wp:push`, `uploads:push`, `plugins:push`, `mu-plugins:push`, `themes:push`, `packages:push`)"); // Pulls all files from remote to local host // Runs wp:pull, uploads:pull, plugins:pull, mu-plugins:pull, themes:pull, packages:pull in series // see tasks definitions for details and required variables task('files:pull', ['wp:pull', 'uploads:pull', 'plugins:pull', 'mu-plugins:pull', 'themes:pull', 'packages:pull']) + // phpcs:ignore Generic.Files.LineLength.TooLong ->desc("Pulls all files from remote to local host (combines `wp:pull`, `uploads:pull`, `plugins:pull`, `mu-plugins:pull`, `themes:pull`, `packages:pull`)"); diff --git a/src/tasks/languages.php b/src/tasks/languages.php index 294a668..0a1562b 100644 --- a/src/tasks/languages.php +++ b/src/tasks/languages.php @@ -13,9 +13,9 @@ use function Deployer\download; use function Deployer\get; use function Deployer\task; -use function \Gaambo\DeployerWordpress\Utils\Files\zipFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pullFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pushFiles; +use function Gaambo\DeployerWordpress\Utils\Files\zipFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pullFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pushFiles; use function Gaambo\DeployerWordpress\Utils\Localhost\getLocalhost; /** diff --git a/src/tasks/mu-plugins.php b/src/tasks/mu-plugins.php index a042c86..a43db73 100644 --- a/src/tasks/mu-plugins.php +++ b/src/tasks/mu-plugins.php @@ -16,9 +16,9 @@ use function Deployer\download; use function Deployer\get; use function Deployer\task; -use function \Gaambo\DeployerWordpress\Utils\Files\zipFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pullFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pushFiles; +use function Gaambo\DeployerWordpress\Utils\Files\zipFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pullFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pushFiles; use function Gaambo\DeployerWordpress\Utils\Localhost\getLocalhost; /** diff --git a/src/tasks/packages.php b/src/tasks/packages.php index 7928994..bd65cd2 100644 --- a/src/tasks/packages.php +++ b/src/tasks/packages.php @@ -19,9 +19,9 @@ use function Deployer\get; use function Deployer\run; use function Deployer\task; -use function \Gaambo\DeployerWordpress\Utils\Files\zipFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pullFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pushFiles; +use function Gaambo\DeployerWordpress\Utils\Files\zipFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pullFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pushFiles; use function Gaambo\DeployerWordpress\Utils\Localhost\getLocalhost; /** diff --git a/src/tasks/plugins.php b/src/tasks/plugins.php index 69ea33f..bd9e6c4 100644 --- a/src/tasks/plugins.php +++ b/src/tasks/plugins.php @@ -13,9 +13,9 @@ use function Deployer\download; use function Deployer\get; use function Deployer\task; -use function \Gaambo\DeployerWordpress\Utils\Files\zipFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pullFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pushFiles; +use function Gaambo\DeployerWordpress\Utils\Files\zipFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pullFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pushFiles; use function Gaambo\DeployerWordpress\Utils\Localhost\getLocalhost; /** diff --git a/src/tasks/themes.php b/src/tasks/themes.php index b6a4c70..187bd8e 100644 --- a/src/tasks/themes.php +++ b/src/tasks/themes.php @@ -17,9 +17,9 @@ use function Deployer\download; use function Deployer\get; use function Deployer\task; -use function \Gaambo\DeployerWordpress\Utils\Files\zipFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pullFiles; -use function \Gaambo\DeployerWordpress\Utils\Files\pushFiles; +use function Gaambo\DeployerWordpress\Utils\Files\zipFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pullFiles; +use function Gaambo\DeployerWordpress\Utils\Files\pushFiles; use function Gaambo\DeployerWordpress\Utils\Localhost\getLocalhost; /** @@ -30,7 +30,10 @@ * - theme/name: Name (= directory) of your custom theme */ task('theme:assets:vendors', function () { - \Gaambo\DeployerWordpress\Utils\Npm\runInstall('{{release_or_current_path}}/{{themes/dir}}/{{theme/name}}', 'install'); + \Gaambo\DeployerWordpress\Utils\Npm\runInstall( + '{{release_or_current_path}}/{{themes/dir}}/{{theme/name}}', + 'install' + ); })->desc("Install theme assets vendors/dependencies (npm)"); /** diff --git a/src/tasks/uploads.php b/src/tasks/uploads.php index aedd0d0..7eb84c2 100644 --- a/src/tasks/uploads.php +++ b/src/tasks/uploads.php @@ -14,7 +14,7 @@ use function Deployer\get; use function Deployer\task; use function Deployer\upload; -use function \Gaambo\DeployerWordpress\Utils\Files\zipFiles; +use function Gaambo\DeployerWordpress\Utils\Files\zipFiles; use function Gaambo\DeployerWordpress\Utils\Localhost\getLocalhost; @@ -23,7 +23,7 @@ * Needs the following variables: * - uploads/filter: rsync filter syntax array of files to push (has a default) * - uploads/dir: Path of uploads directory relative to release_path/current_path - * - uploads/path: Path to directory which contains the uploads directory on remote (eg shared directory, has a default) + * - uploads/path: Path to directory which contains the uploads directory (eg shared directory, has a default) */ task('uploads:push', function () { $localUploadsPath = getLocalhost()->get('uploads/path'); @@ -44,7 +44,7 @@ * Needs the following variables: * - uploads/filter: rsync filter syntax array of files to pull (has a default) * - uploads/dir: Path of uploads directory relative to release_path/current_path - * - uploads/path: Path to directory which contains the uploads directory on remote (eg shared directory, has a default) + * - uploads/path: Path to directory which contains the uploads directory (eg shared directory, has a default) */ task('uploads:pull', function () { $localUploadsPath = getLocalhost()->get('uploads/path'); @@ -66,7 +66,7 @@ * Backup uploads on remote host and downloads zip to local backup path * Needs the following variables: * - uploads/dir: Path of uploads directory relative to release_path/current_path - * - uploads/path: Path to directory which contains the uploads directory on remote (eg shared directory, has a default) + * - uploads/path: Path to directory which contains the uploads directory (eg shared directory, has a default) * - backup_path (on remote host): Path to directory in which to store all backups * - backup_path (on localhost): Path to directory in which to store all backups */ @@ -84,7 +84,7 @@ * Backup uploads on localhost * Needs the following variables: * - uploads/dir: Path of uploads directory relative to release_path/current_path - * - uploads/path: Path to directory which contains the uploads directory on remote (eg shared directory, has a default) + * - uploads/path: Path to directory which contains the uploads directory (eg shared directory, has a default) * - backup_path (on localhost): Path to directory in which to store all backups */ task('uploads:backup:local', function () { diff --git a/src/tasks/wp.php b/src/tasks/wp.php index 73bb0bf..5dc6489 100644 --- a/src/tasks/wp.php +++ b/src/tasks/wp.php @@ -95,4 +95,5 @@ $sudo = get('sudo', false); installWPCLI($installPath, $binaryFile, $sudo); + // phpcs:ignore Generic.Files.LineLength.TooLong })->desc("Install the WP-CLI binary manually with the `wp:install-wpcli` task and set the path as `/bin/wp` afterwards."); diff --git a/src/utils/files.php b/src/utils/files.php index 50bff84..8568e94 100644 --- a/src/utils/files.php +++ b/src/utils/files.php @@ -7,9 +7,9 @@ namespace Gaambo\DeployerWordpress\Utils\Files; -use function \Deployer\upload; -use function \Deployer\download; -use function \Deployer\run; +use function Deployer\upload; +use function Deployer\download; +use function Deployer\run; use function Gaambo\DeployerWordpress\Utils\Localhost\getLocalhost; require_once 'localhost.php'; From d3a44de2d4b542e81b2b2e7411393619bf25d136 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Fri, 7 Mar 2025 16:32:58 +0100 Subject: [PATCH 06/36] updated changelog for v3.2.0 and readme --- CHANGELOG.md | 25 ++++++++++++++++++++ README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aa993f..0637385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## v@next- YYYY-MM-DD + +### Added +- Introduced a new **packages** system for managing custom themes, plugins, and mu-plugins. This system allows for more flexible handling of assets, vendors, and deployment tasks. +- Improved path handling for various components. +- New tasks for deploying/syncing **language files**. +- Examples for Bedrock and simple deployment recipes to help users better understand and implement the new features. + +### Changed +- Updated tasks to integrate with the new packages system, providing more flexibility and control over deployment processes. +- Refactored code to improve consistency and maintainability, particularly in handling local and remote paths. + +### Deprecated +- Existing theme and plugin tasks remain functional, but the packages system offers enhanced capabilities and is the preferred method moving forward. + +### Removed +- advanced recipe (because it was basically the same as the simple) + +**Upgrading:** +- Review and update your `set.php` configuration to include the new `packages` array for managing custom themes, plugins, and mu-plugins. +- Ensure that all paths in your configuration are correctly set relative to `release_path` or `current_path`. +- Consider migrating existing theme and plugin configurations to the new packages system for better flexibility and control. +- Test your deployment process in a staging environment to ensure compatibility with the new changes. +- Read the [README.md](README.md) section about packages for detailed guidance on configuration and usage. + ## v3.1.0 - Added a `deploy:build_assets` step into the default deploy task to build theme assets on local. diff --git a/README.md b/README.md index bf17963..58e29cf 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ A collection of [Deployer](https://deployer.org) Tasks/Recipes to deploy WordPre - [Plugin Tasks (`tasks/plugins.php`)](#plugin-tasks-taskspluginsphp) - [MU Plugin Tasks (`tasks/mu-plugins.php`)](#mu-plugin-tasks-tasksmu-pluginsphp) - [WordPress Tasks (`tasks/wp.php`)](#wordpress-tasks-taskswpphp) + - [Language Tasks (`tasks/languages.php`)](#language-tasks-taskslanguagesphp) - [WP-CLI](#wp-cli) - [Recipes](#recipes) - [Base](#base) @@ -30,6 +31,10 @@ A collection of [Deployer](https://deployer.org) Tasks/Recipes to deploy WordPre - [Contributing](#contributing) - [Testing](#testing) - [Built by](#built-by) + - [Packages](#packages) + - [Configuration](#configuration) + - [Example](#example) + - [Best Practices](#best-practices) ## Installation @@ -123,6 +128,8 @@ You can also run `dep list` to see all available tasks and their description. ### Theme Tasks (`tasks/theme.php`) +> **Note:** It is recommended to use the new packages functionality for managing themes for better flexibility and control. + - `theme:assets:vendors`: Install theme assets vendors/dependencies (npm), can be run locally or remote - `theme:assets:build`: Run theme assets (npm) build script, can be run locally or remote - `theme:assets`: A combined tasks to build theme assets - combines `theme:assets:vendors` and `theme:assets:build` @@ -144,6 +151,8 @@ You can also run `dep list` to see all available tasks and their description. ### Plugin Tasks (`tasks/plugins.php`) +> **Note:** It is recommended to use the new packages functionality for managing plugins for better flexibility and control. + - `plugins:push`: Push plugins from local to remote - `plugins:pull`: Pull plugins from remote to local - `plugins:sync`: Syncs plugins between remote and local @@ -152,6 +161,8 @@ You can also run `dep list` to see all available tasks and their description. ### MU Plugin Tasks (`tasks/mu-plugins.php`) +> **Note:** It is recommended to use the new packages functionality for managing mu-plugins for better flexibility and control. + - `mu-plugin:vendors`: Install mu-plugin vendors (composer), can be run locally or remote - `mu-plugin`: A combined tasks to prepare the theme - at the moment only runs mu-plugin:vendors task - `mu-plugins:push`: Push mu-plugins from local to remote @@ -183,6 +194,14 @@ See [original PR](https://github.com/gaambo/deployer-wordpress/pull/5) for more There's a task for downloading core and `--info`. You can generate your own tasks to handle other WP-CLI commands, there's a util function `Gaambo\DeployerWordpress\Utils\WPCLI\runCommand` (`src/utils/wp-cli.php`); +### Language Tasks (`tasks/languages.php`) + +- `languages:push`: Push language files from local to remote +- `languages:pull`: Pull language files from remote to local +- `languages:sync`: Sync language files between remote and local +- `languages:backup:remote`: Backup language files on remote host and download zip +- `languages:backup:local`: Backup language files on localhost + ## Recipes Deployer WordPress ships with two base recipes which handle my use cases. @@ -229,4 +248,48 @@ Pull requests are always welcome. PSR2 coding standard are used and I try to adh ## Built by -[Gaambo](https://github.com/gaambo) and [Contributors](https://github.com/gaambo/deployer-wordpress/graphs/contributors) \ No newline at end of file +[Gaambo](https://github.com/gaambo) and [Contributors](https://github.com/gaambo/deployer-wordpress/graphs/contributors) + +## Packages (added in v@next) + +The new packages system allows for more flexible handling of custom themes, plugins, and mu-plugins. Packages can be configured to manage assets, vendors, and deployment tasks. + +### Configuration + +To configure packages, add the following to your `set.php` or equivalent configuration file: + +```php +set('packages', [ + [ + 'path' => 'path/to/package', + 'remote:path' => 'path/on/remote', + 'assets' => true, + 'assets:build_script' => 'build', + ], + // Add more packages as needed +]); +``` + +### Example + +Here is an example configuration for a custom theme package: + +```php +set('packages', [ + 'custom-theme' => [ + 'path' => '{{themes/dir}}/custom-theme', + 'remote:path' => '{{themes/dir}}/custom-theme', + 'assets' => true, + 'assets:build_script' => 'build' + ], + 'core-functionality' => [ + 'path' => '{{mu-plugins/dir}}/core-functionality', + 'remote:path' => '{{mu-plugins/dir}}/core-functionality' + ], +]); +``` + +### Best Practices + +- Ensure all package paths are correctly set relative to the `release_path` or `current_path`. +- Define npm build scripts in your `package.json` for asset management. \ No newline at end of file From 44cbd8f249dc3a3b8ae78f4f6173a0451dd9f430 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Sun, 23 Mar 2025 13:59:53 +0100 Subject: [PATCH 07/36] Refactor project structure by removing deprecated files and updating autoloading. Enhance .gitignore to exclude additional files and directories. Transition from file-based autoloading to PSR-4 autoloading for better organization and maintainability. --- .gitignore | 7 +- autoload.php | 3 - composer.json | 6 +- recipes/bedrock.php | 8 +- recipes/common.php | 192 +++++++++++++++++++++++++++++++--- src/Composer.php | 74 +++++++++++++ src/Files.php | 65 ++++++++++++ src/Localhost.php | 32 ++++++ src/NPM.php | 58 +++++++++++ src/Rsync.php | 144 +++++++++++++++++++++++++ src/Utils.php | 31 ++++++ src/WPCLI.php | 73 +++++++++++++ src/set.php | 171 ------------------------------ src/tasks/database.php | 130 ----------------------- src/tasks/files.php | 31 ------ src/tasks/languages.php | 87 ---------------- src/tasks/mu-plugins.php | 112 -------------------- src/tasks/packages.php | 182 -------------------------------- src/tasks/plugins.php | 87 ---------------- src/tasks/themes.php | 147 -------------------------- src/tasks/uploads.php | 98 ----------------- src/tasks/wp.php | 99 ------------------ src/utils/composer.php | 76 -------------- src/utils/files.php | 77 -------------- src/utils/helper.php | 23 ---- src/utils/localhost.php | 19 ---- src/utils/npm.php | 58 ----------- src/utils/rsync.php | 149 -------------------------- src/utils/wp-cli.php | 45 -------- tasks/database.php | 151 +++++++++++++++++++++++++++ tasks/files.php | 108 +++++++++++++++++++ tasks/languages.php | 110 ++++++++++++++++++++ tasks/mu-plugins.php | 139 +++++++++++++++++++++++++ tasks/packages.php | 220 +++++++++++++++++++++++++++++++++++++++ tasks/plugins.php | 110 ++++++++++++++++++++ tasks/themes.php | 198 +++++++++++++++++++++++++++++++++++ tasks/uploads.php | 125 ++++++++++++++++++++++ tasks/wp.php | 114 ++++++++++++++++++++ 38 files changed, 1943 insertions(+), 1616 deletions(-) delete mode 100644 autoload.php create mode 100644 src/Composer.php create mode 100644 src/Files.php create mode 100644 src/Localhost.php create mode 100644 src/NPM.php create mode 100644 src/Rsync.php create mode 100644 src/Utils.php create mode 100644 src/WPCLI.php delete mode 100644 src/set.php delete mode 100644 src/tasks/database.php delete mode 100644 src/tasks/files.php delete mode 100644 src/tasks/languages.php delete mode 100644 src/tasks/mu-plugins.php delete mode 100644 src/tasks/packages.php delete mode 100644 src/tasks/plugins.php delete mode 100644 src/tasks/themes.php delete mode 100644 src/tasks/uploads.php delete mode 100644 src/tasks/wp.php delete mode 100644 src/utils/composer.php delete mode 100644 src/utils/files.php delete mode 100644 src/utils/helper.php delete mode 100644 src/utils/localhost.php delete mode 100644 src/utils/npm.php delete mode 100644 src/utils/rsync.php delete mode 100644 src/utils/wp-cli.php create mode 100644 tasks/database.php create mode 100644 tasks/files.php create mode 100644 tasks/languages.php create mode 100644 tasks/mu-plugins.php create mode 100644 tasks/packages.php create mode 100644 tasks/plugins.php create mode 100644 tasks/themes.php create mode 100644 tasks/uploads.php create mode 100644 tasks/wp.php diff --git a/.gitignore b/.gitignore index ad20b9a..864f912 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ -/vendor/ -/test \ No newline at end of file +/vendor +/tests +/.cursor +/.idea +.DS_STORE \ No newline at end of file diff --git a/autoload.php b/autoload.php deleted file mode 100644 index 09a7739..0000000 --- a/autoload.php +++ /dev/null @@ -1,3 +0,0 @@ -hasOwn('php_version')) { + return '/usr/bin/php{{php_version}}'; + } + return which('php'); +}); + +// can be overwritten if you eg. use wpcli in a docker container +set('bin/wp', function () { + $installPath = '{{deploy_path}}/.dep'; + $binaryFile = 'wp-cli.phar'; + + if (test("[ -f $installPath/$binaryFile ]")) { + return "{{bin/php}} $installPath/$binaryFile"; + } + + if (commandExist('wp')) { + return '{{bin/php}} ' . which('wp'); + } + + warning("WP-CLI binary wasn't found. Installing latest WP-CLI to $installPath/$binaryFile."); + WPCLI::install($installPath, $binaryFile); + return "{{bin/php}} $installPath/$binaryFile"; +}); + +set('composer_action', 'install'); +set('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader'); + +// Returns Composer binary path in found. Otherwise try to install latest +// composer version to `.dep/composer.phar`. To use specific composer version +// download desired phar and place it at `.dep/composer.phar`. +set('bin/composer', function () { + $installPath = '{{deploy_path}}/.dep'; + $binaryFile = 'composer.phar'; + + if (test("[ -f $installPath/$binaryFile ]")) { + return "{{bin/php}} $installPath/$binaryFile"; + } + + if (commandExist('composer')) { + return '{{bin/php}} ' . which('composer'); + } + + warning("Composer binary wasn't found. Installing latest composer to $installPath/$binaryFile."); + Composer::install($installPath, $binaryFile); + return "{{bin/php}} $installPath/$binaryFile"; +}); + +// PATHS & FILES CONFIGURATION + +// if you want to further define options for rsyncing files +// just look at the source in `Files.php` and `Rsync.php` +// and use the Rsync::buildOptionsArray and Files::push/pull methods +set('wp/dir', ''); // relative to document root +// config files which should be protected - add to shared_files as well +set('wp/configFiles', ['wp-config.php', 'wp-config-local.php']); +// set all wp-config files to 600 - which means plugins/wordpress can modify it +// alternative set it to 400 to disallow edits via wordpress +set('wp/configFiles/permissions', '600'); +set('wp/filter', [ // contains all wordpress core files excluding uploads, themes, plugins, mu-plugins, languages + '+ /wp-content/', + '- /wp-content/mu-plugins/*', + '- /wp-content/plugins/*', + '- /wp-content/themes/*', + '- /wp-content/uploads/*', + '- /wp-content/languages/*', + '- /wp-content/upgrade', + '- /wp-content/cache', + '+ /wp-content/**', // all other files in wp-content + '+ /wp-admin/', + '+ /wp-admin/**', + '+ /wp-includes/', + '+ /wp-includes/**', + '+ wp-activate.php', + '+ wp-blog-header.php', + '+ wp-comments-post.php', + '+ wp-config-sample.php', + '+ wp-config.php', + '- wp-config-local.php', // should be required in wp-config.php + '+ wp-cron.php', + '+ wp-links-opml.php', + '+ wp-load.php', + '+ wp-login.php', + '+ wp-mail.php', + '+ wp-settings.php', + '+ wp-signup.php', + '+ wp-trackback.php', + '+ xmlrpc.php', + '+ index.php', + '- *' +]); +set('uploads/dir', 'wp-content/uploads'); // relative to document root +set('uploads/path', '{{release_or_current_path}}'); // path in front of uploads directory +set('uploads/filter', []); // rsync filter syntax +set('mu-plugins/dir', 'wp-content/mu-plugins'); // relative to document root +set('mu-plugins/filter', []); // rsync filter syntax +set('plugins/dir', 'wp-content/plugins'); // relative to document root +set('plugins/filter', []); // rsync filter syntax +set('themes/dir', 'wp-content/themes'); // relative to document root +set('themes/filter', []); // rsync filter syntax +set('theme/build_script', 'build'); // custom theme npm build script +set('languages/dir', 'wp-content/languages'); // relative to document root +set('languages/filter', []); // rsync filter syntax + +// options for zipping files for backups - passed to zip shell command +set('zip_options', '-x "_backup_*.zip" -x **/node_modules/**\* -x **/vendor/**\*'); + +// SHARED FILES +set('shared_files', ['wp-config.php', 'wp-config-local.php']); +set('shared_dirs', ['{{uploads/dir}}']); +set('writable_dirs', ['{{uploads/dir}}']); + +// The default rsync config +// used by all *:push/*:pull tasks and in `src/utils/rsync.php:buildOptionsArray` +set('rsync', function () { + $config = [ + 'exclude' => [], // do NOT exclude .deployfilter files - remote should be aware of them + 'exclude-file' => false, + 'include' => [], + 'include-file' => false, + 'filter' => [], + 'filter-file' => false, + // Allows specifying (=excluding/including/filtering) files to sync per directory in a `.deployfilter` file + // See README directory for examples + 'filter-perdir' => '.deployfilter', + 'flags' => 'rz', // Recursive, with compress + 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterwards + 'timeout' => 60, + 'progress_bar' => true, + ]; + + if (output()->isVerbose()) { + $config['options'][] = 'verbose'; + } + if (output()->isVeryVerbose()) { + $config['options'][] = 'verbose'; + } + if (output()->isDebug()) { + $config['options'][] = 'verbose'; + } + + return $config; +}); +// https://github.com/deployphp/deployer/issues/3139 +set('rsync_src', __DIR__); + +set('release_name', function () { + return date('YmdHis'); // you could also use the composer.json version here +}); + +set('writable_mode', 'chown'); // Overwrite deploy:info task to show host instead of branch task('deploy:info', function () { @@ -53,7 +219,7 @@ // Build package assets via npm locally task('deploy:build_assets', function () { - on(getLocalhost(), function () { + on(Localhost::get(), function () { if (has('packages')) { invoke('packages:assets:vendors'); invoke('packages:assets:build'); @@ -96,8 +262,8 @@ */ task('cache:clear', function () { // TODO: overwrite, maybe clear cache via wpcli - // run("cd {{release_or_current_path}} && {{bin/wp}} rocket clean --confirm"); - // run("cd {{release_or_current_path}} && {{bin/wp}} cache flush"); + // WPCLI::runCommand("rocket clean --confirm", "{{release_or_current_path}}"); + // WPCLI::runCommand("cache flush", "{{release_or_current_path}}"); }); /** diff --git a/src/Composer.php b/src/Composer.php new file mode 100644 index 0000000..2c75241 --- /dev/null +++ b/src/Composer.php @@ -0,0 +1,74 @@ + $rsyncOptions]); + } + + /** + * Pull files from remote to local + * @param string $remotePath Remote path to pull from + * @param string $localPath Local path to pull to + * @param array $rsyncOptions Rsync options array + * @return void + */ + public static function pullFiles(string $remotePath, string $localPath, array $rsyncOptions = []): void + { + $localPath = Localhost::getConfig('current_path') . '/' . $localPath; + download('{{release_or_current_path}}/' . $remotePath . '/', $localPath . '/', ['options' => $rsyncOptions]); + } + + /** + * Zip files into a backup zip + * + * @param string $dir Directory to zip + * Can have a trailing slash, which backups the contents of the directory, if not it backups the directory into the zip + * @param string $backupDir Directory in which to store the zip + * @param string $filename Filename of the zip file - gets prefixed to a datetime + * @return string The full path ($backupDir + full filename) to the created zip + */ + public static function zipFiles(string $dir, string $backupDir, string $filename): string + { + $backupFilename = $filename . '_' . date('Y-m-d_H-i-s') . '.zip'; + $backupPath = "$backupDir/$backupFilename"; + run("mkdir -p $backupDir"); + + // dir can have a trailing slash (which means, backup only the content of the specified directory) + if (substr($dir, -1) == '/') { + // Add everything from directory to zip, but exclude previous backups + run("cd {$dir} && zip -r {$backupFilename} . {{zip_options}} && mv $backupFilename $backupPath"); + } else { + $parentDir = dirname($dir); + $dir = basename($dir); + // Add dir itself to zip, but exclude previous backups + run("cd {$parentDir} && zip -r {$backupFilename} {$dir} {{zip_options}} && mv $backupFilename $backupPath"); + } + + return $backupPath; + } +} \ No newline at end of file diff --git a/src/Localhost.php b/src/Localhost.php new file mode 100644 index 0000000..2d98577 --- /dev/null +++ b/src/Localhost.php @@ -0,0 +1,32 @@ +get($key); + } + + /** + * Get the (single) defined localhost host + * + * @return Host Localhost host + */ + public static function get(): Host + { + return \Deployer\Deployer::get()->hosts->get('localhost'); + } +} \ No newline at end of file diff --git a/src/NPM.php b/src/NPM.php new file mode 100644 index 0000000..2ce9a44 --- /dev/null +++ b/src/NPM.php @@ -0,0 +1,58 @@ + (array) Array of paths/files to exclude + * 'exclude-file' => (string) Path to a exclude file to pass to rsync + * 'include' => (array) Array of paths/files to include + * 'include-file' => (string) Path to a include file to pass to rsync + * 'filter' => (array) Array of rsync filters + * 'filter-file' => (string) Path to a filterFile in rsync filter syntax + * 'filter-perdir' => (string) Filename to be used on a per directory basis for rsync filtering + * 'options' => (array) Array of options/flags to be passed to rsync - do not include dashes! + * ] + * @return array + */ + public static function buildOptionsArray(array $config = []): array + { + $defaultConfig = get('rsync'); + if (!$defaultConfig || !is_array($defaultConfig)) { + $defaultConfig = [ + 'exclude' => [ + '.git', + 'deploy.php', + ], + 'exclude-file' => false, + 'include' => [], + 'include-file' => false, + 'filter' => [], + 'filter-file' => false, + 'filter-perdir' => false, + 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterwards + ]; + } + + $mergedConfig = array_merge($defaultConfig, $config); + + $options = array_merge( + self::buildOptions($mergedConfig['options']), + self::buildIncludes($mergedConfig['include'], $mergedConfig['include-file']), + self::buildExcludes($mergedConfig['exclude'], $mergedConfig['exclude-file']), + self::buildFilter($mergedConfig['filter'], $mergedConfig['filter-file'], $mergedConfig['filter-perdir']) + ); + + // remove empty strings because they break rsync + // because Rsync class uses escapeshellarg + return array_filter($options); + } +} \ No newline at end of file diff --git a/src/Utils.php b/src/Utils.php new file mode 100644 index 0000000..c4311f6 --- /dev/null +++ b/src/Utils.php @@ -0,0 +1,31 @@ +isVerbose()) { + $verbosityArgument = '-v'; + } + if ($outputInterface->isVeryVerbose()) { + $verbosityArgument = '-vv'; + } + if ($outputInterface->isDebug()) { + $verbosityArgument = '-vvv'; + } + + return $verbosityArgument; + } +} \ No newline at end of file diff --git a/src/WPCLI.php b/src/WPCLI.php new file mode 100644 index 0000000..e95cbde --- /dev/null +++ b/src/WPCLI.php @@ -0,0 +1,73 @@ +hasOwn('php_version')) { - return '/usr/bin/php{{php_version}}'; - } - return which('php'); -}); - -// can be overwritten if you eg. use wpcli in a docker container -set('bin/wp', function () { - $installPath = '{{deploy_path}}/.dep'; - $binaryFile = 'wp-cli.phar'; - - if (test("[ -f $installPath/$binaryFile ]")) { - return "{{bin/php}} $installPath/$binaryFile"; - } - - if (commandExist('wp')) { - return '{{bin/php}} ' . which('wp'); - } - - warning("WP-CLI binary wasn't found. Installing latest WP-CLI to $installPath/$binaryFile."); - installWPCLI($installPath, $binaryFile); - return "{{bin/php}} $installPath/$binaryFile"; -}); - -set('composer_action', 'install'); -set('composer_options', '--verbose --prefer-dist --no-progress --no-interaction --no-dev --optimize-autoloader'); - -// Returns Composer binary path in found. Otherwise try to install latest -// composer version to `.dep/composer.phar`. To use specific composer version -// download desired phar and place it at `.dep/composer.phar`. -set('bin/composer', function () { - $installPath = '{{deploy_path}}/.dep'; - $binaryFile = 'composer.phar'; - - if (test("[ -f $installPath/$binaryFile ]")) { - return "{{bin/php}} $installPath/$binaryFile"; - } - - if (commandExist('composer')) { - return '{{bin/php}} ' . which('composer'); - } - - warning("Composer binary wasn't found. Installing latest composer to $installPath/$binaryFile."); - installComposer($installPath, $binaryFile); - return "{{bin/php}} $installPath/$binaryFile"; -}); - -// PATHS & FILES CONFIGURATION - -// if you want to further define options for rsyncing files -// just look at the source in `files.php` -// and use the Rsync\buildConfig, Files\pushFiles and Files\pullFiles utils methods -set('wp/dir', ''); // relative to document root -// config files which should be protected - add to shared_files as well -set('wp/configFiles', ['wp-config.php', 'wp-config-local.php']); -// set all wp-config files to 600 - which means plugins/wordpress can modify it -// alternative set it to 400 to disallow edits via wordpress -set('wp/configFiles/permissions', '600'); -set('wp/filter', [ // contains all wordpress core files excluding uploads, themes, plugins, mu-plugins, languages - '+ /wp-content/', - '- /wp-content/mu-plugins/*', - '- /wp-content/plugins/*', - '- /wp-content/themes/*', - '- /wp-content/uploads/*', - '- /wp-content/languages/*', - '- /wp-content/upgrade', - '- /wp-content/cache', - '+ /wp-content/**', // all other files in wp-content - '+ /wp-admin/', - '+ /wp-admin/**', - '+ /wp-includes/', - '+ /wp-includes/**', - '+ wp-activate.php', - '+ wp-blog-header.php', - '+ wp-comments-post.php', - '+ wp-config-sample.php', - '+ wp-config.php', - '- wp-config-local.php', // should be required in wp-config.php - '+ wp-cron.php', - '+ wp-links-opml.php', - '+ wp-load.php', - '+ wp-login.php', - '+ wp-mail.php', - '+ wp-settings.php', - '+ wp-signup.php', - '+ wp-trackback.php', - '+ xmlrpc.php', - '+ index.php', - '- *' -]); -set('uploads/dir', 'wp-content/uploads'); // relative to document root -set('uploads/path', '{{release_or_current_path}}'); // path in front of uploads directory -set('uploads/filter', []); // rsync filter syntax -set('mu-plugins/dir', 'wp-content/mu-plugins'); // relative to document root -set('mu-plugins/filter', []); // rsync filter syntax -set('plugins/dir', 'wp-content/plugins'); // relative to document root -set('plugins/filter', []); // rsync filter syntax -set('themes/dir', 'wp-content/themes'); // relative to document root -set('themes/filter', []); // rsync filter syntax -set('theme/build_script', 'build'); // custom theme npm build script -set('languages/dir', 'wp-content/languages'); // relative to document root -set('languages/filter', []); // rsync filter syntax - -// options for zipping files for backups - passed to zip shell command -set('zip_options', '-x "_backup_*.zip" -x **/node_modules/**\* -x **/vendor/**\*'); - -// SHARED FILES -set('shared_files', ['wp-config.php', 'wp-config-local.php']); -set('shared_dirs', ['{{uploads/dir}}']); -set('writable_dirs', ['{{uploads/dir}}']); - -// The default rsync config -// used by all *:push/*:pull tasks and in `src/utils/rsync.php:buildOptionsArray` -set('rsync', function () { - $config = [ - 'exclude' => [], // do NOT exclude .deployfilter files - remote should be aware of them - 'exclude-file' => false, - 'include' => [], - 'include-file' => false, - 'filter' => [], - 'filter-file' => false, - // Allows specifying (=excluding/including/filtering) files to sync per directory in a `.deployfilter` file - // See README directory for examples - 'filter-perdir' => '.deployfilter', - 'flags' => 'rz', // Recursive, with compress - 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterwards - 'timeout' => 60, - 'progress_bar' => true, - ]; - - if (output()->isVerbose()) { - $config['options'][] = 'verbose'; - } - if (output()->isVeryVerbose()) { - $config['options'][] = 'verbose'; - } - if (output()->isDebug()) { - $config['options'][] = 'verbose'; - } - - return $config; -}); -// https://github.com/deployphp/deployer/issues/3139 -set('rsync_src', __DIR__); - -set('release_name', function () { - return date('YmdHis'); // you could also use the composer.json version here -}); - -set('writable_mode', 'chown'); diff --git a/src/tasks/database.php b/src/tasks/database.php deleted file mode 100644 index 763be71..0000000 --- a/src/tasks/database.php +++ /dev/null @@ -1,130 +0,0 @@ -get('dump_path'); - $now = date('Y-m-d_H-i', time()); - set('dump_file', "db_backup-$now.sql"); - set('dump_filepath', get('dump_path') . '/' . get('dump_file')); - - run('mkdir -p ' . get('dump_path')); - run("cd {{release_or_current_path}} && {{bin/wp}} db export {{dump_filepath}} --add-drop-table"); - - runLocally("mkdir -p $localDumpPath"); - download('{{dump_filepath}}', "$localDumpPath/{{dump_file}}"); -})->desc('Backup remote database and download'); - -/** - * Backup local database and upload to remote host - * Needs the following variables: - * - dump_path on localhost: Path in which to store database dumps/backups - * - bin/wp on localhost: WP CLI binary/command to use (has a default) - * dump_path on remote host get's set in this task - * Backup gets deleted in push task - * @todo check if this (especially dump_path in shared) works with simple recipe - */ -task('db:local:backup', function () { - $localWp = getLocalhost()->get('bin/wp'); - $localDumpPath = getLocalhost()->get('dump_path'); - $now = date('Y-m-d_H-i', time()); - set('dump_file', "db_backup-$now.sql"); - set('dump_filepath', '{{dump_path}}/{{dump_file}}'); - - runLocally("mkdir -p $localDumpPath"); - runLocally("$localWp db export $localDumpPath/{{dump_file}} --add-drop-table"); - - run('mkdir -p {{dump_path}}'); - upload( - "$localDumpPath/{{dump_file}}", - '{{dump_filepath}}' - ); -})->desc('Backup local database and upload'); - -/** - * Import current database backup (from localhost) on remote host - * Needs the following variables: - * - bin/wp on remote host: WP CLI binary/command to use (has a default) - * - public_url on localhost an remote host: To replace in database - * dump_filepath is set in db:local:backup task and gets deleted after importing - */ -task('db:remote:import', function () { - $localUrl = getLocalhost()->get('public_url'); - run("cd {{release_or_current_path}} && {{bin/wp}} db import {{dump_filepath}}"); - run("cd {{release_or_current_path}} && {{bin/wp}} search-replace $localUrl {{public_url}}"); - - // If the local uploads directory is different than the remote one - // replace all references to the local uploads directory with the remote one - $localUploadsDir = getLocalhost()->get('uploads/dir'); - if ($localUploadsDir !== get('uploads/dir')) { - run("cd {{release_or_current_path}} && {{bin/wp}} search-replace $localUploadsDir {{uploads/dir}}"); - } - - run('rm -f {{dump_filepath}}'); -})->desc('Imports Database on remote host'); - -/** - * Import current database backup (from remote host) on local host - * Needs the following variables: - * - bin/wp on localhost: WP CLI binary/command to use (has a default) - * - public_url on localhost an remote host: To replace in database - * - dump_path on localhost: Path in which backups are stored - * dump_filepath is set in db:local:backup task and gets deleted after importing - */ -task('db:local:import', function () { - $localWp = getLocalhost()->get('bin/wp'); - $localUrl = getLocalhost()->get('public_url'); - $localDumpPath = getLocalhost()->get('dump_path'); - runLocally("$localWp db import $localDumpPath/{{dump_file}}"); - runLocally("$localWp search-replace {{public_url}} $localUrl"); - - // If the local uploads directory is different than the remote one - // replace all references to the remotes uploads directory with the local one - $localUploadsDir = getLocalhost()->get('uploads/dir'); - if ($localUploadsDir !== get('uploads/dir')) { - run("cd {{release_or_current_path}} && {{bin/wp}} search-replace {{uploads/dir}} $localUploadsDir"); - } - - runLocally("rm -f $localDumpPath/{{dump_file}}"); -})->desc('Imports Database on local host'); - -/** - * Pushes local database to remote host - * Runs db:local:backup and db:remote:import tasks in series - * See tasks definitions for required variables - */ -task('db:push', ['db:local:backup', 'db:remote:import']) - ->desc("Pushes local database to remote host (combines `db:local:backup` and `db:remote:import`)"); - -/** - * Pulls remote database to localhost - * Runs db:remote:backup and db:local:import tasks in series - * See tasks definitions for required variables - */ -task('db:pull', ['db:remote:backup', 'db:local:import']) - ->desc("Pulls remote database to localhost (combines `db:remote:backup` and `db:local:import`)"); diff --git a/src/tasks/files.php b/src/tasks/files.php deleted file mode 100644 index ab0d15a..0000000 --- a/src/tasks/files.php +++ /dev/null @@ -1,31 +0,0 @@ -desc("Pushes all files from local to remote host (combines push for wp, uploads, plugins, mu-plugins, themes, packages`wp:push`, `uploads:push`, `plugins:push`, `mu-plugins:push`, `themes:push`, `packages:push`)"); - -// Pulls all files from remote to local host -// Runs wp:pull, uploads:pull, plugins:pull, mu-plugins:pull, themes:pull, packages:pull in series -// see tasks definitions for details and required variables -task('files:pull', ['wp:pull', 'uploads:pull', 'plugins:pull', 'mu-plugins:pull', 'themes:pull', 'packages:pull']) - // phpcs:ignore Generic.Files.LineLength.TooLong - ->desc("Pulls all files from remote to local host (combines `wp:pull`, `uploads:pull`, `plugins:pull`, `mu-plugins:pull`, `themes:pull`, `packages:pull`)"); diff --git a/src/tasks/languages.php b/src/tasks/languages.php deleted file mode 100644 index 0a1562b..0000000 --- a/src/tasks/languages.php +++ /dev/null @@ -1,87 +0,0 @@ - get("languages/filter"), - ]); - pushFiles(getLocalhost()->get('languages/dir'), '{{languages/dir}}', $rsyncOptions); -})->desc('Push languages from local to remote'); - -/** - * Pull languages from remote to local - * Needs the following variables: - * - languages/filter: rsync filter syntax array of files to pull (has a default) - * - languages/dir: Path of languages directory relative to release_path/current_path - * - deploy_path or release_path: to build remote path - */ -task('languages:pull', function () { - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("languages/filter"), - ]); - pullFiles('{{languages/dir}}', getLocalhost()->get('languages/dir'), $rsyncOptions); -})->desc('Pull languages from remote to local'); - -/** - * Syncs languages between remote and local - * Runs languages:push and languages:pull tasks in series - * See tasks definitions for required variables - */ -task("languages:sync", ["languages:push", "languages:pull"])->desc("Sync languages"); - -/** - * Backup languages on remote host and downloads zip to local backup path - * Needs the following variables: - * - languages/dir: Path of languages directory relative to release_path/current_path - * - backup_path (on remote host): Path to directory in which to store all backups - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('languages:backup:remote', function () { - $backupFile = zipFiles( - "{{release_or_current_path}}/{{languages/dir}}/", - '{{backup_path}}', - 'backup_languages' - ); - $localBackupPath = getLocalhost()->get('backup_path'); - download($backupFile, "$localBackupPath/"); -})->desc('Backup languages on remote host and download zip'); - -/** - * Backup languages on localhost - * Needs the following variables: - * - languages/dir: Path of languages directory relative to release_path/current_path - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('languages:backup:local', function () { - $localPath = getLocalhost()->get('current_path'); - $localBackupPath = getLocalhost()->get('backup_path'); - $backupFile = zipFiles( - "$localPath/{{languages/dir}}/", - $localBackupPath, - 'backup_languages' - ); -})->once()->desc('Backup local languages as zip'); diff --git a/src/tasks/mu-plugins.php b/src/tasks/mu-plugins.php deleted file mode 100644 index a43db73..0000000 --- a/src/tasks/mu-plugins.php +++ /dev/null @@ -1,112 +0,0 @@ -desc("Install mu-plugin vendors (composer)"); - -/** - * Install mu-plugin vendors (composer) - * At the moment only runs mu-plugin:vendors task - * See task definition for required variables - */ -task('mu-plugin', ['mu-plugin:vendors']) - ->desc("A combined tasks to prepare the theme"); - -/** - * Push mu-plugins from local to remote - * Needs the following variables: - * - mu-plugins/filter: rsync filter syntax array of files to push (has a default) - * - mu-plugins/dir: Path of mu-plugins directory relative to release_path/current_path - * - deploy_path or release_path: to build remote path - */ -task('mu-plugins:push', function () { - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("mu-plugins/filter"), - ]); - pushFiles(getLocalhost()->get('mu-plugins/dir'), '{{mu-plugins/dir}}', $rsyncOptions); -})->desc('Push mu-plugins from local to remote'); - -/** - * Pull mu-plugins from remote to local - * Needs the following variables: - * - mu-plugins/filter: rsync filter syntax array of files to pull (has a default) - * - mu-plugins/dir: Path of mu-plugins directory relative to release_path/current_path - * - deploy_path or release_path: to build remote path - */ -task('mu-plugins:pull', function () { - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("mu-plugins/filter"), - ]); - pullFiles('{{mu-plugins/dir}}', getLocalhost()->get('mu-plugins/dir'), $rsyncOptions); -})->desc('Pull mu-plugins from remote to local'); - -/** - * Syncs mu-plugins between remote and local - * Runs mu-plugins:push and mu-plugins:pull tasks in series - * See tasks definitions for required variables - */ -task("mu-plugins:sync", ["mu-plugins:push", "mu-plugins:pull"])->desc("Sync mu-plugins"); - -/** - * Backup mu-plugins on remote host and download zip to local backup path - * Needs the following variables: - * - mu-plugins/dir: Path of mu-plugins directory relative to release_path/current_path - * - backup_path (on remote host): Path to directory in which to store all backups - * - backup_path (on localhost): Path to directory in which to store all backups - * - deploy_path or release_path: to build remote path - */ -task('mu-plugins:backup:remote', function () { - $backupFile = zipFiles( - "{{release_or_current_path}}/{{mu-plugins/dir}}/", - '{{backup_path}}', - 'backup_mu-plugins' - ); - $localBackupPath = getLocalhost()->get('backup_path'); - download($backupFile, "$localBackupPath/"); -})->desc('Backup mu-plugins on remote host and download zip'); - -/** - * Backup mu-plugins on localhost - * Needs the following variables: - * - mu-plugins/dir: Path of mu-plugins directory relative to release_path/current_path - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('mu-plugins:backup:local', function () { - $localPath = getLocalhost()->get('mu-plugins/dir'); - $localBackupPath = getLocalhost()->get('backup_path'); - $backupFile = zipFiles( - "$localPath/", - $localBackupPath, - 'backup_mu-plugins' - ); -})->once()->desc('Backup local mu-plugins as zip'); diff --git a/src/tasks/packages.php b/src/tasks/packages.php deleted file mode 100644 index bd65cd2..0000000 --- a/src/tasks/packages.php +++ /dev/null @@ -1,182 +0,0 @@ -desc("Install package assets vendors/dependencies (npm)"); - -/** - * Run package assets (npm) build script - * - * Needs the following config per package: - * - 'path' (string): Path of package relative to release_path/current_path - * - (optional) assets:build_script: NPM script to be run (must be defined in package.json, has a default of "build") - */ -task('packages:assets:build', function () { - foreach (get('packages', []) as $package) { - $packagePath = $package['path']; - if (empty($package['assets'])) { - continue; - } - \Gaambo\DeployerWordpress\Utils\Npm\runScript( - "{{release_or_current_path}}/$packagePath", - $package['assets:build_script'] ?? 'build' - ); - } -})->desc("Run package assets (npm) build script"); - -/** - * Install package assets vendors/dependencies (npm) and run build script - * Runs package:assets:vendors and package:assets:build tasks in series - */ -task('packages:assets', ['packages:assets:vendors', 'packages:assets:build']) - ->desc("A combined task to prepare the packages assets - combines `packages:assets` and `packages:vendors`"); - -/** - * Install packages vendors (composer) - * - * Needs the following config per package: - * - 'path' (string): Path of package relative to release_path/current_path - */ -task('packages:vendors', function () { - foreach (get('packages', []) as $package) { - $packagePath = $package['path']; - \Gaambo\DeployerWordpress\Utils\Composer\runDefault( - "{{release_or_current_path}}/$packagePath" - ); - } -})->desc("Install packages vendors (composer)"); - -/** - * Install packages vendors (composer + npm) and build assets (npm) - * - * Runs packages:assets and packages:vendors tasks in series - * See tasks definitions for required variables - */ -task('packages', ['packages:assets', 'packages:vendors']) - ->desc("A combined task to prepare the packages - combines `packages:assets` and `packages:vendors`"); - -/** - * Push packages from local to remote - * - * Needs the following config per package: - * - 'path' (string): Path of package relative to release_path/current_path - * - 'remote:path' (string): Path of package on remote host - * - (optional) 'rsync:filter' (array): rsync filter syntax array of files to push - */ -task('packages:push', function () { - foreach (get('packages', []) as $package) { - $packagePath = $package['path']; - $remotePath = $package['remote:path']; - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => $package['rsync:filter'] ?? [], - ]); - run("mkdir -p {{release_or_current_path}}/$remotePath"); - pushFiles($packagePath, $remotePath, $rsyncOptions); - } -})->desc('Push packages from local to remote'); - -/** - * Pull packages from remote to local - * - * Needs the following config per package: - * - 'path' (string): Path of package relative to release_path/current_path - * - 'remote:path' (string): Path of package on remote host - * - (optional) 'rsync:filter' (array): rsync filter syntax array of files to push - */ -task('packages:pull', function () { - foreach (get('packages', []) as $package) { - $packagePath = $package['path']; - $remotePath = $package['remote:path']; - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => $package['rsync:filter'] ?? [], - ]); - pullFiles($remotePath, $packagePath, $rsyncOptions); - } -})->desc('Pull packages from remote to local'); - -/** - * Syncs packages between remote and local - * - * Runs packages:push and packages:pull tasks in series - * See tasks definitions for required variables - */ -task("packages:sync", ["packages:push", "packages:pull"])->desc("Sync packages"); - -/** - * Backup packages on remote host and downloads zip to local backup path - * - * Needs the following config per package: - * - 'remote:path' (string): Path of package on remote host - * - backup_path (on remote host): Path to directory in which to store all backups - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('packages:backup:remote', function () { - foreach (get('packages', []) as $package) { - $remotePath = $package['remote:path']; - $backupFile = zipFiles( - "{{release_or_current_path}}/$remotePath/", - '{{backup_path}}', - 'backup_packages' - ); - $localBackupPath = getLocalhost()->get('backup_path'); - download($backupFile, "$localBackupPath/"); - } -})->desc('Backup packages on remote host and download zip'); - -/** - * Backup packages on local host - * - * Needs the following config per package: - * - 'remote:path' (string): Path of package on remote host - * - backup_path (on remote host): Path to directory in which to store all backups - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('packages:backup:local', function () { - $localPath = getLocalhost()->get('current_path'); - $localBackupPath = getLocalhost()->get('backup_path'); - - foreach (get('packages', []) as $package) { - $packagePath = $package['path']; - $backupFile = zipFiles( - "{{release_or_current_path}}/$packagePath/", - $localBackupPath, - 'backup_packages' - ); - } -})->once()->desc('Backup local packages as zip'); diff --git a/src/tasks/plugins.php b/src/tasks/plugins.php deleted file mode 100644 index bd9e6c4..0000000 --- a/src/tasks/plugins.php +++ /dev/null @@ -1,87 +0,0 @@ - get("plugins/filter"), - ]); - pushFiles(getLocalhost()->get('plugins/dir'), '{{plugins/dir}}', $rsyncOptions); -})->desc('Push plugins from local to remote'); - -/** - * Pull plugins from remote to local - * Needs the following variables: - * - plugins/filter: rsync filter syntax array of files to pull (has a default) - * - plugins/dir: Path of plugins directory relative to release_path/current_path - * - deploy_path or release_path: to build remote path - */ -task('plugins:pull', function () { - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("plugins/filter"), - ]); - pullFiles('{{plugins/dir}}', getLocalhost()->get('plugins/dir'), $rsyncOptions); -})->desc('Pull plugins from remote to local'); - -/** - * Syncs plugins between remote and local - * Runs plugins:push and plugins:pull tasks in series - * See tasks definitions for required variables - */ -task("plugins:sync", ["plugins:push", "plugins:pull"])->desc("Sync plugins"); - -/** - * Backup plugins on remote host and downloads zip to local backup path - * Needs the following variables: - * - plugins/dir: Path of plugins directory relative to release_path/current_path - * - backup_path (on remote host): Path to directory in which to store all backups - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('plugins:backup:remote', function () { - $backupFile = zipFiles( - "{{release_or_current_path}}/{{plugins/dir}}/", - '{{backup_path}}', - 'backup_plugins' - ); - $localBackupPath = getLocalhost()->get('backup_path'); - download($backupFile, "$localBackupPath/"); -})->desc('Backup plugins on remote host and download zip'); - -/** - * Backup plugins on localhost - * Needs the following variables: - * - plugins/dir: Path of plugins directory relative to release_path/current_path - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('plugins:backup:local', function () { - $localPath = getLocalhost()->get('current_path'); - $localBackupPath = getLocalhost()->get('backup_path'); - $backupFile = zipFiles( - "$localPath/{{plugins/dir}}/", - $localBackupPath, - 'backup_plugins' - ); -})->once()->desc('Backup local plugins as zip'); diff --git a/src/tasks/themes.php b/src/tasks/themes.php deleted file mode 100644 index 187bd8e..0000000 --- a/src/tasks/themes.php +++ /dev/null @@ -1,147 +0,0 @@ -desc("Install theme assets vendors/dependencies (npm)"); - -/** - * Run theme assets (npm) build script - * Can be run locally or remote - * Needs the following variables: - * - themes/dir: Path to directory which contains all themes relative to release_path/current_path - * - theme/name: Name (= directory) of your custom theme - * - theme/build_script: NPM script to be run (must be defined in package.json, has a default) - */ -task('theme:assets:build', function () { - \Gaambo\DeployerWordpress\Utils\Npm\runScript( - '{{release_or_current_path}}/{{themes/dir}}/{{theme/name}}', - '{{theme/build_script}}' - ); -})->desc("Run theme assets (npm) build script"); - -/** - * Install theme assets vendors/dependencies (npm) and run build script - * Runs theme:assets:vendors and theme:assets:build tasks in series - */ -task('theme:assets', ['theme:assets:vendors', 'theme:assets:build']) - ->desc("A combined task to prepare the theme - combines `theme:assets` and `theme:vendors`"); - -/** - * Install theme vendors (composer) - * Can be run locally or remote - * Needs the following variables: - * - themes/dir: Path to directory which contains all themes relative to release_path/current_path - * - theme/name: Name (= directory) of your custom theme - */ -task('theme:vendors', function () { - \Gaambo\DeployerWordpress\Utils\Composer\runDefault('{{release_or_current_path}}/{{themes/dir}}/{{theme/name}}'); -})->desc("Install theme vendors (composer), can be run locally or remote"); - -/** - * Install theme vendors (composer + npm) and build assets (npm) - * Runs theme:assets and theme:vendors tasks in series - * See tasks definitions for required variables - */ -task('theme', ['theme:assets', 'theme:vendors']) - ->desc("A combined task to prepare the theme - combines `theme:assets` and `theme:vendors`"); - -/** - * Push themes from local to remote - * Needs the following variables: - * - themes/filter: rsync filter syntax array of files to push (has a default) - * - themes/dir: Path of themes directory relative to release_path/current_path - * - deploy_path or release_path: to build remote path - */ -task('themes:push', function () { - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("themes/filter"), - ]); - pushFiles(getLocalhost()->get('themes/dir'), '{{themes/dir}}', $rsyncOptions); -})->desc('Push themes from local to remote'); - -/** - * Pull themes from remote to local - * Needs the following variables: - * - themes/filter: rsync filter syntax array of files to pull (has a default) - * - themes/dir: Path of themes directory relative to release_path/current_path - * - deploy_path or release_path: to build remote path - */ -task('themes:pull', function () { - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("themes/filter"), - ]); - pullFiles('{{themes/dir}}', getLocalhost()->get('themes/dir'), $rsyncOptions); -})->desc('Pull themes from remote to local'); - -/** - * Syncs themes between remote and local - * Runs themes:push and themes:pull tasks in series - * See tasks definitions for required variables - */ -task("themes:sync", ["themes:push", "themes:pull"])->desc("Sync themes"); - -/** - * Backup themes on remote host and downloads zip to local backup path - * Needs the following variables: - * - themes/dir: Path of themes directory relative to release_path/current_path - * - backup_path (on remote host): Path to directory in which to store all backups - * - backup_path (on localhost): Path to directory in which to store all backups - * - deploy_path or release_path: to build remote path - */ -task('themes:backup:remote', function () { - $backupFile = zipFiles( - "{{release_or_current_path}}/{{themes/dir}}/", - '{{backup_path}}', - 'backup_themes' - ); - $localBackupPath = getLocalhost()->get('backup_path'); - download($backupFile, "$localBackupPath/"); -})->desc('Backup themes on remote host and download zip'); - -/** - * Backup themes on localhost - * Needs the following variables: - * - themes/dir: Path of themes directory relative to release_path/current_path - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('themes:backup:local', function () { - $localPath = getLocalhost()->get('current_path'); - $localBackupPath = getLocalhost()->get('backup_path'); - $backupFile = zipFiles( - "$localPath/{{themes/dir}}/", - $localBackupPath, - 'backup_themes' - ); -})->once()->desc('Backup local themes as zip'); diff --git a/src/tasks/uploads.php b/src/tasks/uploads.php deleted file mode 100644 index 7eb84c2..0000000 --- a/src/tasks/uploads.php +++ /dev/null @@ -1,98 +0,0 @@ -get('uploads/path'); - $localUploadsDir = getLocalhost()->get('uploads/dir'); - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("uploads/filter"), - ]); - - upload( - "$localUploadsPath/$localUploadsDir/", - '{{uploads/path}}/{{uploads/dir}}/', - ['options' => $rsyncOptions] - ); -})->desc('Push uploads from local to remote'); - -/** - * Pull uploads from remote to local - * Needs the following variables: - * - uploads/filter: rsync filter syntax array of files to pull (has a default) - * - uploads/dir: Path of uploads directory relative to release_path/current_path - * - uploads/path: Path to directory which contains the uploads directory (eg shared directory, has a default) - */ -task('uploads:pull', function () { - $localUploadsPath = getLocalhost()->get('uploads/path'); - $localUploadsDir = getLocalhost()->get('uploads/dir'); - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("uploads/filter"), - ]); - download("{{uploads/path}}/{{uploads/dir}}/", "$localUploadsPath/$localUploadsDir/", ['options' => $rsyncOptions]); -})->desc('Pull uploads from remote to local'); - -/** - * Syncs uploads between remote and local - * Runs uploads:push and uploads:pull tasks in series - * See tasks definitions for required variables - */ -task("uploads:sync", ["uploads:push", "uploads:pull"])->desc("Sync uploads"); - -/** - * Backup uploads on remote host and downloads zip to local backup path - * Needs the following variables: - * - uploads/dir: Path of uploads directory relative to release_path/current_path - * - uploads/path: Path to directory which contains the uploads directory (eg shared directory, has a default) - * - backup_path (on remote host): Path to directory in which to store all backups - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('uploads:backup:remote', function () { - $backupFile = zipFiles( - '{{uploads/path}}/{{uploads/dir}}/', - '{{backup_path}}', - 'backup_uploads' - ); - $localBackupPath = getLocalhost()->get('backup_path'); - download($backupFile, "$localBackupPath/"); -})->desc('Backup uploads on remote host and download zip'); - -/** - * Backup uploads on localhost - * Needs the following variables: - * - uploads/dir: Path of uploads directory relative to release_path/current_path - * - uploads/path: Path to directory which contains the uploads directory (eg shared directory, has a default) - * - backup_path (on localhost): Path to directory in which to store all backups - */ -task('uploads:backup:local', function () { - $localPath = getLocalhost()->get('current_path'); - $localBackupPath = getLocalhost()->get('backup_path'); - $backupFile = zipFiles( - "$localPath/{{uploads/dir}}/", - $localBackupPath, - 'backup_uploads' - ); -})->once()->desc('Backup local uploads as zip'); diff --git a/src/tasks/wp.php b/src/tasks/wp.php deleted file mode 100644 index 5dc6489..0000000 --- a/src/tasks/wp.php +++ /dev/null @@ -1,99 +0,0 @@ -desc('Installs a WordPress version via WP CLI'); - -/** - * Pushes WordPress core files via rsync - * Needs the following variables: - * - deploy_path or release_path: to build remote path - * - wp/filter: rsync filter syntax array of files to push (has a default) - * - wp/dir: Path of WordPress directory relative to release_path/current_path - */ -task('wp:push', function () { - $localWpDir = getLocalhost()->get('wp/dir'); - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("wp/filter"), - 'flags' => 'rz', - ]); - \Gaambo\DeployerWordpress\Utils\Files\pushFiles( - $localWpDir, - '{{wp/dir}}', - $rsyncOptions - ); -})->desc('Push WordPress core files from local to remote'); - -/** - * Pulls WordPress core files via rsync - * Needs the following variables: - * - deploy_path or release_path: to build remote path - * - wp/filter: rsync filter syntax array of files to push (has a default) - * - wp/dir: Path of WordPress directory relative to release_path/current_path - */ -task('wp:pull', function () { - $localWpDir = getLocalhost()->get('wp/dir'); - $rsyncOptions = \Gaambo\DeployerWordpress\Utils\Rsync\buildOptionsArray([ - 'filter' => get("wp/filter"), - 'flags' => 'rz', - ]); - \Gaambo\DeployerWordpress\Utils\Files\pullFiles( - '{{wp/dir}}', - $localWpDir, - $rsyncOptions - ); -})->desc('Pull WordPress core files from remote to local'); - -/** - * Runs the --info command via WP CLI - * Just a helper/test task - * Needs the following variables: - * - deploy_path or release_path: to build remote path - * - bin/wp: WP CLI binary/command to use (has a default) - */ -task('wp:info', function () { - runCommand("--info"); -})->desc("Runs the --info command via WP CLI - just a helper/test task"); - -/** - * Installs the WP-CLI binary - for usage via CLI - * Pass installPath, binaryFile and sudo via CLI like so: - * `dep wp:install-wpcli production -o installPath=/usr/local/bin -o binaryFile=wp -o sudo=true` - */ -task('wp:install-wpcli', function () { - $installPath = get('installPath'); - if (empty($installPath)) { - throw new \RuntimeException( - 'You have to set an installPath for WordPress via a config variable or in cli via `-o installPath=$path`.' - ); - } - $binaryFile = get('binaryFile', 'wp-cli.phar'); - $sudo = get('sudo', false); - - installWPCLI($installPath, $binaryFile, $sudo); - // phpcs:ignore Generic.Files.LineLength.TooLong -})->desc("Install the WP-CLI binary manually with the `wp:install-wpcli` task and set the path as `/bin/wp` afterwards."); diff --git a/src/utils/composer.php b/src/utils/composer.php deleted file mode 100644 index 2ae0915..0000000 --- a/src/utils/composer.php +++ /dev/null @@ -1,76 +0,0 @@ -get('current_path'); - upload("$localPath/$source/", "{{release_or_current_path}}/$destination/", ['options' => $rsyncOptions]); -} - -/** - * Pull files from a remote host - * Syncs from a directory in remotes release_path/current_path to locals current_path - * Uses Deployers download function - * - * @param string $source Source path relative to release_path/current_path - * @param string $destination Destination path relative to locals current_path - * @param array $rsyncOptions Array of command-line arguments for rsync to pass to Deployers download - * @return void - */ -function pullFiles(string $source, string $destination, array $rsyncOptions) -{ - $localPath = getLocalhost()->get('current_path'); - download("{{release_or_current_path}}/$source/", "$localPath/$destination/", ['options' => $rsyncOptions]); -} - -/** - * Zip files into a backup zip - * - * @param string $dir Directory to zip - * Can have a trailing slash, which backups the contents of the directory, if not it backups the directory into the zip - * @param string $backupDir Directory in which to store the zip - * @param string $filename Filename of the zip file - gets prefixed to a datetime - * @return string The full path ($backupDir + full filename) to the created zip - */ -function zipFiles(string $dir, string $backupDir, string $filename): string -{ - - $backupFilename = $filename . '_' . date('Y-m-d_H-i-s') . '.zip'; - $backupPath = "$backupDir/$backupFilename"; - run("mkdir -p $backupDir"); - - // dir can have a trailing slash (which means, backup only the content of the specified directory) - if (substr($dir, -1) == '/') { - // Add everything from directory to zip, but exclude previous backups - run("cd {$dir} && zip -r {$backupFilename} . {{zip_options}} && mv $backupFilename $backupPath"); - } else { - $parentDir = dirname($dir); - $dir = basename($dir); - // Add dir itself to zip, but exclude previous backups - run("cd {$parentDir} && zip -r {$backupFilename} {$dir} {{zip_options}} && mv $backupFilename $backupPath"); - } - - return $backupPath; -} diff --git a/src/utils/helper.php b/src/utils/helper.php deleted file mode 100644 index a402ac7..0000000 --- a/src/utils/helper.php +++ /dev/null @@ -1,23 +0,0 @@ -isVerbose()) { - $verbosityArgument = '-v'; - } - if ($outputInterface->isVeryVerbose()) { - $verbosityArgument = '-vv'; - } - if ($outputInterface->isDebug()) { - $verbosityArgument = '-vvv'; - } - return $verbosityArgument; -} diff --git a/src/utils/localhost.php b/src/utils/localhost.php deleted file mode 100644 index 54200bd..0000000 --- a/src/utils/localhost.php +++ /dev/null @@ -1,19 +0,0 @@ -hosts->get('localhost'); -} diff --git a/src/utils/npm.php b/src/utils/npm.php deleted file mode 100644 index 2887ff5..0000000 --- a/src/utils/npm.php +++ /dev/null @@ -1,58 +0,0 @@ - (array) Array of paths/files to exclude - * 'exclude-file' => (string) Path to a exclude file to pass to rsync - * 'include' => (array) Array of paths/files to include - * 'include-file' => (string) Path to a include file to pass to rsync - * 'filter' => (array) Array of rsync filters - * 'filter-file' => (string) Path to a filterFile in rsync filter syntax - * 'filter-perdir' => (string) Filename to be used on a per directory basis for rsync filtering - * 'options' => (array) Array of options/flags to be passed to rsync - do not include dashes! - * ] - * @return array - */ -function buildOptionsArray(array $config = []): array -{ - $defaultConfig = \Deployer\get('rsync'); - if (!$defaultConfig || !is_array($defaultConfig)) { - $defaultConfig = [ - 'exclude' => [ - '.git', - 'deploy.php', - ], - 'exclude-file' => false, - 'include' => [], - 'include-file' => false, - 'filter' => [], - 'filter-file' => false, - 'filter-perdir' => false, - 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterwards - ]; - } - - - $mergedConfig = array_merge($defaultConfig, $config); - - $options = array_merge( - buildOptions($mergedConfig['options']), - buildIncludes($mergedConfig['include'], $mergedConfig['include-file']), - buildExcludes($mergedConfig['exclude'], $mergedConfig['exclude-file']), - buildFilter($mergedConfig['filter'], $mergedConfig['filter-file'], $mergedConfig['filter-perdir']) - ); - - // remove empty strings because they break rsync - // because Rsync class uses escapeshellarg - return array_filter($options); -} diff --git a/src/utils/wp-cli.php b/src/utils/wp-cli.php deleted file mode 100644 index 9c28dfe..0000000 --- a/src/utils/wp-cli.php +++ /dev/null @@ -1,45 +0,0 @@ -desc('Create backup of remote database and download locally'); + +/** + * Create backup of local database and upload to remote + * + * Configuration: + * - dump_path: Directory to store database dumps (required on both local and remote) + * - bin/wp: WP-CLI binary/command to use (automatically configured) + * + * Example: + * dep db:local:backup prod + */ +task('db:local:backup', function () { + $localDumpPath = Localhost::getConfig('dump_path'); + $now = date('Y-m-d_H-i', time()); + set('dump_file', "db_backup-$now.sql"); + set('dump_filepath', '{{dump_path}}/{{dump_file}}'); + + runLocally("mkdir -p $localDumpPath"); + WPCLI::runCommandLocally("db export $localDumpPath/{{dump_file}} --add-drop-table"); + + run('mkdir -p {{dump_path}}'); + upload( + "$localDumpPath/{{dump_file}}", + '{{dump_filepath}}' + ); +})->desc('Create backup of local database and upload to remote'); + +/** + * Import database backup on remote host + * + * Configuration: + * - bin/wp: WP-CLI binary/command to use (automatically configured) + * - public_url: Site URL for both local and remote (required for URL replacement) + * - uploads/dir: Upload directory path (for path replacement if different between environments) + * + * Example: + * dep db:remote:import prod + */ +task('db:remote:import', function () { + $localUrl = Localhost::getConfig('public_url'); + WPCLI::runCommand("db import {{dump_filepath}}", "{{release_or_current_path}}"); + WPCLI::runCommand("search-replace $localUrl {{public_url}}", "{{release_or_current_path}}"); + + // If the local uploads directory is different than the remote one + // replace all references to the local uploads directory with the remote one + $localUploadsDir = Localhost::getConfig('uploads/dir'); + if ($localUploadsDir !== get('uploads/dir')) { + WPCLI::runCommand("search-replace $localUploadsDir {{uploads/dir}}", "{{release_or_current_path}}"); + } + + run('rm -f {{dump_filepath}}'); +})->desc('Import database backup on remote host'); + +/** + * Import database backup on local host + * + * Configuration: + * - bin/wp: WP-CLI binary/command to use (automatically configured) + * - public_url: Site URL for both local and remote (required for URL replacement) + * - uploads/dir: Upload directory path (for path replacement if different between environments) + * - dump_path: Directory containing database dumps + * + * Example: + * dep db:local:import prod + */ +task('db:local:import', function () { + $localUrl = Localhost::getConfig('public_url'); + $localDumpPath = Localhost::getConfig('dump_path'); + WPCLI::runCommandLocally("db import $localDumpPath/{{dump_file}}"); + WPCLI::runCommandLocally("search-replace {{public_url}} $localUrl"); + + // If the local uploads directory is different than the remote one + // replace all references to the remotes uploads directory with the local one + $localUploadsDir = Localhost::getConfig('uploads/dir'); + if ($localUploadsDir !== get('uploads/dir')) { + WPCLI::runCommandLocally("search-replace {{uploads/dir}} $localUploadsDir"); + } + + runLocally("rm -f $localDumpPath/{{dump_file}}"); +})->desc('Import database backup on local host'); + +/** + * Push database from local to remote + * + * Combines db:local:backup and db:remote:import tasks. + * See individual tasks for configuration options. + * + * Example: + * dep db:push prod + */ +task('db:push', ['db:local:backup', 'db:remote:import']) + ->desc('Push database from local to remote'); + +/** + * Pull database from remote to local + * + * Combines db:remote:backup and db:local:import tasks. + * See individual tasks for configuration options. + * + * Example: + * dep db:pull prod + */ +task('db:pull', ['db:remote:backup', 'db:local:import']) + ->desc('Pull database from remote to local'); diff --git a/tasks/files.php b/tasks/files.php new file mode 100644 index 0000000..9dd4341 --- /dev/null +++ b/tasks/files.php @@ -0,0 +1,108 @@ +desc('Push all files from local to remote'); + +/** + * Pull all files from remote to local + * + * Runs wp:pull, uploads:pull, plugins:pull, mu-plugins:pull, themes:pull, packages:pull in series. + * See individual task definitions for required configuration options. + * + * Example: + * dep files:pull prod + */ +task('files:pull', ['wp:pull', 'uploads:pull', 'plugins:pull', 'mu-plugins:pull', 'themes:pull', 'packages:pull']) + ->desc('Pull all files from remote to local'); + +/** + * Sync all files between remote and local + * + * Combines files:push and files:pull tasks. + * See individual tasks for configuration options. + * + * Example: + * dep files:sync prod + */ +task('files:sync', ['files:push', 'files:pull']) + ->desc('Sync all files between environments'); + +/** + * Backup all files on remote host + * + * Creates a zip backup of remote WordPress files and downloads it locally. + * + * Configuration: + * - backup_path: Path for storing backups (required on both local and remote) + * - release_path: Path to WordPress installation on remote + * + * Example: + * dep files:backup:remote prod + */ +task('files:backup:remote', function () { + $backupFile = Files::zipFiles( + '{{release_or_current_path}}/', + '{{backup_path}}', + 'backup_files' + ); + $localBackupPath = Localhost::getConfig('backup_path'); + download($backupFile, "$localBackupPath/"); +})->desc('Backup remote files and download locally'); + +/** + * Backup all files on local host + * + * Creates a zip backup of local WordPress files. + * + * Configuration: + * - backup_path: Path for storing backups (local) + * - current_path: Path to WordPress installation on local + * + * Example: + * dep files:backup:local prod + */ +task('files:backup:local', function () { + $localPath = Localhost::getConfig('current_path'); + $localBackupPath = Localhost::getConfig('backup_path'); + $backupFile = Files::zipFiles( + "$localPath/", + $localBackupPath, + 'backup_files' + ); +})->once()->desc('Backup local files'); diff --git a/tasks/languages.php b/tasks/languages.php new file mode 100644 index 0000000..b5609fe --- /dev/null +++ b/tasks/languages.php @@ -0,0 +1,110 @@ + get("languages/filter"), + ]); + Files::pushFiles(Localhost::getConfig('languages/dir'), '{{languages/dir}}', $rsyncOptions); +})->desc('Push languages from local to remote'); + +/** + * Pull languages from remote to local + * + * Configuration: + * - languages/dir: Path to languages directory relative to document root + * - languages/filter: Rsync filter rules for language files (has defaults) + * + * Example: + * dep languages:pull prod + */ +task('languages:pull', function () { + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => get("languages/filter"), + ]); + Files::pullFiles('{{languages/dir}}', Localhost::getConfig('languages/dir'), $rsyncOptions); +})->desc('Pull languages from remote to local'); + +/** + * Sync languages between remote and local + * + * Combines languages:push and languages:pull tasks. + * See individual tasks for configuration options. + * + * Example: + * dep languages:sync prod + */ +task('languages:sync', ['languages:push', 'languages:pull']) + ->desc('Sync languages between environments'); + +/** + * Backup languages on remote host + * + * Creates a zip backup of remote languages and downloads it locally. + * + * Configuration: + * - languages/dir: Path to languages directory relative to document root + * - backup_path: Path for storing backups (required on both local and remote) + * + * Example: + * dep languages:backup:remote prod + */ +task('languages:backup:remote', function () { + $backupFile = Files::zipFiles( + '{{release_or_current_path}}/{{languages/dir}}/', + '{{backup_path}}', + 'backup_languages' + ); + $localBackupPath = Localhost::getConfig('backup_path'); + download($backupFile, "$localBackupPath/"); +})->desc('Backup remote languages and download locally'); + +/** + * Backup languages on local host + * + * Creates a zip backup of local languages. + * + * Configuration: + * - languages/dir: Path to languages directory relative to document root + * - backup_path: Path for storing backups (local) + * + * Example: + * dep languages:backup:local prod + */ +task('languages:backup:local', function () { + $localPath = Localhost::getConfig('current_path'); + $localBackupPath = Localhost::getConfig('backup_path'); + $backupFile = Files::zipFiles( + "$localPath/{{languages/dir}}/", + $localBackupPath, + 'backup_languages' + ); +})->once()->desc('Backup local languages'); diff --git a/tasks/mu-plugins.php b/tasks/mu-plugins.php new file mode 100644 index 0000000..05c246a --- /dev/null +++ b/tasks/mu-plugins.php @@ -0,0 +1,139 @@ +desc('Install mu-plugin vendors via composer'); + +/** + * Install all mu-plugin dependencies + * + * Currently only runs mu-plugin:vendors task. + * See individual tasks for configuration options. + * + * Example: + * dep mu-plugin prod + */ +task('mu-plugin', ['mu-plugin:vendors']) + ->desc('Install all mu-plugin dependencies'); + +/** + * Push mu-plugins from local to remote + * + * Configuration: + * - mu-plugins/dir: Path to mu-plugins directory relative to document root + * - mu-plugins/filter: Rsync filter rules for mu-plugin files (has defaults) + * + * Example: + * dep mu-plugins:push prod + */ +task('mu-plugins:push', function () { + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => get("mu-plugins/filter"), + ]); + Files::pushFiles(Localhost::getConfig('mu-plugins/dir'), '{{mu-plugins/dir}}', $rsyncOptions); +})->desc('Push mu-plugins from local to remote'); + +/** + * Pull mu-plugins from remote to local + * + * Configuration: + * - mu-plugins/dir: Path to mu-plugins directory relative to document root + * - mu-plugins/filter: Rsync filter rules for mu-plugin files (has defaults) + * + * Example: + * dep mu-plugins:pull prod + */ +task('mu-plugins:pull', function () { + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => get("mu-plugins/filter"), + ]); + Files::pullFiles('{{mu-plugins/dir}}', Localhost::getConfig('mu-plugins/dir'), $rsyncOptions); +})->desc('Pull mu-plugins from remote to local'); + +/** + * Sync mu-plugins between remote and local + * + * Combines mu-plugins:push and mu-plugins:pull tasks. + * See individual tasks for configuration options. + * + * Example: + * dep mu-plugins:sync prod + */ +task('mu-plugins:sync', ['mu-plugins:push', 'mu-plugins:pull']) + ->desc('Sync mu-plugins between environments'); + +/** + * Backup mu-plugins on remote host + * + * Creates a zip backup of remote mu-plugins and downloads it locally. + * + * Configuration: + * - mu-plugins/dir: Path to mu-plugins directory relative to document root + * - backup_path: Path for storing backups (required on both local and remote) + * + * Example: + * dep mu-plugins:backup:remote prod + */ +task('mu-plugins:backup:remote', function () { + $backupFile = Files::zipFiles( + '{{release_or_current_path}}/{{mu-plugins/dir}}/', + '{{backup_path}}', + 'backup_mu-plugins' + ); + $localBackupPath = Localhost::getConfig('backup_path'); + download($backupFile, "$localBackupPath/"); +})->desc('Backup remote mu-plugins and download locally'); + +/** + * Backup mu-plugins on local host + * + * Creates a zip backup of local mu-plugins. + * + * Configuration: + * - mu-plugins/dir: Path to mu-plugins directory relative to document root + * - backup_path: Path for storing backups (local) + * + * Example: + * dep mu-plugins:backup:local prod + */ +task('mu-plugins:backup:local', function () { + $localPath = Localhost::getConfig('current_path'); + $localBackupPath = Localhost::getConfig('backup_path'); + $backupFile = Files::zipFiles( + "$localPath/{{mu-plugins/dir}}/", + $localBackupPath, + 'backup_mu-plugins' + ); +})->once()->desc('Backup local mu-plugins'); diff --git a/tasks/packages.php b/tasks/packages.php new file mode 100644 index 0000000..2908d4b --- /dev/null +++ b/tasks/packages.php @@ -0,0 +1,220 @@ +desc('Install package assets dependencies via npm'); + +/** + * Build package assets via npm script + * + * Configuration per package: + * - path: Path of package relative to release_path/current_path + * - assets: Whether the package has assets to build + * - assets:build_script: NPM script to run (optional, default: "build") + * + * Example: + * dep packages:assets:build prod + */ +task('packages:assets:build', function () { + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + if (empty($package['assets'])) { + continue; + } + NPM::runScript( + "{{release_or_current_path}}/$packagePath", + $package['assets:build_script'] ?? 'build' + ); + } +})->desc('Build package assets via npm script'); + +/** + * Install and build package assets + * + * Combines packages:assets:vendors and packages:assets:build tasks. + * See individual tasks for configuration options. + * + * Example: + * dep packages:assets prod + */ +task('packages:assets', ['packages:assets:vendors', 'packages:assets:build']) + ->desc('Install and build package assets'); + +/** + * Install package dependencies via composer + * + * Configuration per package: + * - path: Path of package relative to release_path/current_path + * + * Example: + * dep packages:vendors prod + */ +task('packages:vendors', function () { + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + Composer::runDefault( + "{{release_or_current_path}}/$packagePath" + ); + } +})->desc('Install package dependencies via composer'); + +/** + * Install all package dependencies + * + * Combines packages:assets and packages:vendors tasks. + * See individual tasks for configuration options. + * + * Example: + * dep packages prod + */ +task('packages', ['packages:assets', 'packages:vendors']) + ->desc('Install all package dependencies'); + +/** + * Push packages from local to remote + * + * Configuration per package: + * - path: Path of package relative to release_path/current_path + * - remote:path: Path of package on remote host + * - rsync:filter: (optional) Rsync filter rules for package files + * + * Example: + * dep packages:push prod + */ +task('packages:push', function () { + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + $remotePath = $package['remote:path']; + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => $package['rsync:filter'] ?? [], + ]); + run("mkdir -p {{release_or_current_path}}/$remotePath"); + Files::pushFiles($packagePath, $remotePath, $rsyncOptions); + } +})->desc('Push packages from local to remote'); + +/** + * Pull packages from remote to local + * + * Configuration per package: + * - path: Path of package relative to release_path/current_path + * - remote:path: Path of package on remote host + * - rsync:filter: (optional) Rsync filter rules for package files + * + * Example: + * dep packages:pull prod + */ +task('packages:pull', function () { + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + $remotePath = $package['remote:path']; + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => $package['rsync:filter'] ?? [], + ]); + Files::pullFiles($remotePath, $packagePath, $rsyncOptions); + } +})->desc('Pull packages from remote to local'); + +/** + * Sync packages between remote and local + * + * Combines packages:push and packages:pull tasks. + * See individual tasks for configuration options. + * + * Example: + * dep packages:sync prod + */ +task('packages:sync', ['packages:push', 'packages:pull']) + ->desc('Sync packages between environments'); + +/** + * Backup packages on remote host + * + * Creates a zip backup of remote packages and downloads it locally. + * + * Configuration per package: + * - remote:path: Path of package on remote host + * - backup_path: Path for storing backups (required on both local and remote) + * + * Example: + * dep packages:backup:remote prod + */ +task('packages:backup:remote', function () { + foreach (get('packages', []) as $package) { + $remotePath = $package['remote:path']; + $backupFile = Files::zipFiles( + "{{release_or_current_path}}/$remotePath/", + '{{backup_path}}', + 'backup_packages' + ); + $localBackupPath = Localhost::getConfig('backup_path'); + download($backupFile, "$localBackupPath/"); + } +})->desc('Backup remote packages and download locally'); + +/** + * Backup packages on local host + * + * Creates a zip backup of local packages. + * + * Configuration per package: + * - path: Path of package relative to release_path/current_path + * - backup_path: Path for storing backups (local) + * + * Example: + * dep packages:backup:local prod + */ +task('packages:backup:local', function () { + $localPath = Localhost::getConfig('current_path'); + $localBackupPath = Localhost::getConfig('backup_path'); + + foreach (get('packages', []) as $package) { + $packagePath = $package['path']; + $backupFile = Files::zipFiles( + "$localPath/$packagePath/", + $localBackupPath, + 'backup_packages' + ); + } +})->once()->desc('Backup local packages'); diff --git a/tasks/plugins.php b/tasks/plugins.php new file mode 100644 index 0000000..8444b29 --- /dev/null +++ b/tasks/plugins.php @@ -0,0 +1,110 @@ + get("plugins/filter"), + ]); + Files::pushFiles(Localhost::getConfig('plugins/dir'), '{{plugins/dir}}', $rsyncOptions); +})->desc('Push plugins from local to remote'); + +/** + * Pull plugins from remote to local + * + * Configuration: + * - plugins/dir: Path to plugins directory relative to document root + * - plugins/filter: Rsync filter rules for plugin files (has defaults) + * + * Example: + * dep plugins:pull prod + */ +task('plugins:pull', function () { + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => get("plugins/filter"), + ]); + Files::pullFiles('{{plugins/dir}}', Localhost::getConfig('plugins/dir'), $rsyncOptions); +})->desc('Pull plugins from remote to local'); + +/** + * Sync plugins between remote and local + * + * Combines plugins:push and plugins:pull tasks. + * See individual tasks for configuration options. + * + * Example: + * dep plugins:sync prod + */ +task('plugins:sync', ['plugins:push', 'plugins:pull']) + ->desc('Sync plugins between environments'); + +/** + * Backup plugins on remote host + * + * Creates a zip backup of remote plugins and downloads it locally. + * + * Configuration: + * - plugins/dir: Path to plugins directory relative to document root + * - backup_path: Path for storing backups (required on both local and remote) + * + * Example: + * dep plugins:backup:remote prod + */ +task('plugins:backup:remote', function () { + $backupFile = Files::zipFiles( + '{{release_or_current_path}}/{{plugins/dir}}/', + '{{backup_path}}', + 'backup_plugins' + ); + $localBackupPath = Localhost::getConfig('backup_path'); + download($backupFile, "$localBackupPath/"); +})->desc('Backup remote plugins and download locally'); + +/** + * Backup plugins on local host + * + * Creates a zip backup of local plugins. + * + * Configuration: + * - plugins/dir: Path to plugins directory relative to document root + * - backup_path: Path for storing backups (local) + * + * Example: + * dep plugins:backup:local prod + */ +task('plugins:backup:local', function () { + $localPath = Localhost::getConfig('current_path'); + $localBackupPath = Localhost::getConfig('backup_path'); + $backupFile = Files::zipFiles( + "$localPath/{{plugins/dir}}/", + $localBackupPath, + 'backup_plugins' + ); +})->once()->desc('Backup local plugins'); diff --git a/tasks/themes.php b/tasks/themes.php new file mode 100644 index 0000000..45b257b --- /dev/null +++ b/tasks/themes.php @@ -0,0 +1,198 @@ +desc('Install theme assets dependencies via npm'); + +/** + * Build theme assets via npm script + * + * Configuration: + * - themes/dir: Path to themes directory relative to document root + * - theme/name: Name (directory) of your custom theme + * - theme/build_script: NPM script to run (default: 'build') + * - bin/npm: NPM binary/command to use (automatically configured) + * + * Example: + * dep theme:assets:build prod + * dep theme:assets:build prod -o theme/build_script=dev + */ +task('theme:assets:build', function () { + NPM::runScript( + '{{release_or_current_path}}/{{themes/dir}}/{{theme/name}}', + '{{theme/build_script}}' + ); +})->desc('Build theme assets via npm script'); + +/** + * Install theme assets and run build script + * + * Combines theme:assets:vendors and theme:assets:build tasks. + * See individual tasks for configuration options. + * + * Example: + * dep theme:assets prod + */ +task('theme:assets', ['theme:assets:vendors', 'theme:assets:build']) + ->desc('Install theme assets and run build script'); + +/** + * Install theme vendors via composer + * + * Configuration: + * - themes/dir: Path to themes directory relative to document root + * - theme/name: Name (directory) of your custom theme + * - bin/composer: Composer binary/command to use (automatically configured) + * + * Example: + * dep theme:vendors prod + */ +task('theme:vendors', function () { + Composer::runDefault('{{release_or_current_path}}/{{themes/dir}}/{{theme/name}}'); +})->desc('Install theme vendors via composer'); + +/** + * Install all theme dependencies and build assets + * + * Combines theme:assets and theme:vendors tasks. + * See individual tasks for configuration options. + * + * Example: + * dep theme prod + */ +task('theme', ['theme:assets', 'theme:vendors']) + ->desc('Install all theme dependencies and build assets'); + +/** + * Push themes from local to remote + * + * Configuration: + * - themes/dir: Path to themes directory relative to document root + * - themes/filter: Rsync filter rules for theme files (has defaults) + * + * Example: + * dep themes:push prod + */ +task('themes:push', function () { + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => get("themes/filter"), + ]); + Files::pushFiles(Localhost::getConfig('themes/dir'), '{{themes/dir}}', $rsyncOptions); +})->desc('Push themes from local to remote'); + +/** + * Pull themes from remote to local + * + * Configuration: + * - themes/dir: Path to themes directory relative to document root + * - themes/filter: Rsync filter rules for theme files (has defaults) + * + * Example: + * dep themes:pull prod + */ +task('themes:pull', function () { + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => get("themes/filter"), + ]); + Files::pullFiles('{{themes/dir}}', Localhost::getConfig('themes/dir'), $rsyncOptions); +})->desc('Pull themes from remote to local'); + +/** + * Sync themes between remote and local + * + * Combines themes:push and themes:pull tasks. + * See individual tasks for configuration options. + * + * Example: + * dep themes:sync prod + */ +task('themes:sync', ['themes:push', 'themes:pull']) + ->desc('Sync themes between environments'); + +/** + * Backup themes on remote host + * + * Creates a zip backup of remote themes and downloads it locally. + * + * Configuration: + * - themes/dir: Path to themes directory relative to document root + * - backup_path: Path for storing backups (required on both local and remote) + * + * Example: + * dep themes:backup:remote prod + */ +task('themes:backup:remote', function () { + $backupFile = Files::zipFiles( + '{{release_or_current_path}}/{{themes/dir}}/', + '{{backup_path}}', + 'backup_themes' + ); + $localBackupPath = Localhost::getConfig('backup_path'); + download($backupFile, "$localBackupPath/"); +})->desc('Backup remote themes and download locally'); + +/** + * Backup themes on local host + * + * Creates a zip backup of local themes. + * + * Configuration: + * - themes/dir: Path to themes directory relative to document root + * - backup_path: Path for storing backups (local) + * + * Example: + * dep themes:backup:local prod + */ +task('themes:backup:local', function () { + $localPath = Localhost::getConfig('current_path'); + $localBackupPath = Localhost::getConfig('backup_path'); + $backupFile = Files::zipFiles( + "$localPath/{{themes/dir}}/", + $localBackupPath, + 'backup_themes' + ); +})->once()->desc('Backup local themes'); diff --git a/tasks/uploads.php b/tasks/uploads.php new file mode 100644 index 0000000..71bb0b4 --- /dev/null +++ b/tasks/uploads.php @@ -0,0 +1,125 @@ + get("uploads/filter"), + ]); + + upload( + "$localUploadsPath/$localUploadsDir/", + '{{uploads/path}}/{{uploads/dir}}/', + ['options' => $rsyncOptions] + ); +})->desc('Push uploads from local to remote'); + +/** + * Pull uploads from remote to local + * + * Configuration: + * - uploads/dir: Path to uploads directory relative to document root + * - uploads/path: Path to directory containing uploads (e.g., shared directory) + * - uploads/filter: Rsync filter rules for upload files (has defaults) + * + * Example: + * dep uploads:pull prod + */ +task('uploads:pull', function () { + $localUploadsPath = Localhost::getConfig('uploads/path'); + $localUploadsDir = Localhost::getConfig('uploads/dir'); + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => get("uploads/filter"), + ]); + download("{{uploads/path}}/{{uploads/dir}}/", "$localUploadsPath/$localUploadsDir/", ['options' => $rsyncOptions]); +})->desc('Pull uploads from remote to local'); + +/** + * Sync uploads between remote and local + * + * Combines uploads:push and uploads:pull tasks. + * See individual tasks for configuration options. + * + * Example: + * dep uploads:sync prod + */ +task('uploads:sync', ['uploads:push', 'uploads:pull']) + ->desc('Sync uploads between environments'); + +/** + * Backup uploads on remote host + * + * Creates a zip backup of remote uploads and downloads it locally. + * + * Configuration: + * - uploads/dir: Path to uploads directory relative to document root + * - uploads/path: Path to directory containing uploads (e.g., shared directory) + * - backup_path: Path for storing backups (required on both local and remote) + * + * Example: + * dep uploads:backup:remote prod + */ +task('uploads:backup:remote', function () { + $backupFile = Files::zipFiles( + '{{uploads/path}}/{{uploads/dir}}/', + '{{backup_path}}', + 'backup_uploads' + ); + $localBackupPath = Localhost::getConfig('backup_path'); + download($backupFile, "$localBackupPath/"); +})->desc('Backup remote uploads and download locally'); + +/** + * Backup uploads on local host + * + * Creates a zip backup of local uploads. + * + * Configuration: + * - uploads/dir: Path to uploads directory relative to document root + * - uploads/path: Path to directory containing uploads (e.g., shared directory) + * - backup_path: Path for storing backups (local) + * + * Example: + * dep uploads:backup:local prod + */ +task('uploads:backup:local', function () { + $localUploadsPath = Localhost::getConfig('uploads/path'); + $localUploadsDir = Localhost::getConfig('uploads/dir'); + $localBackupPath = Localhost::getConfig('backup_path'); + $backupFile = Files::zipFiles( + "$localUploadsPath/$localUploadsDir/", + $localBackupPath, + 'backup_uploads' + ); +})->once()->desc('Backup local uploads'); diff --git a/tasks/wp.php b/tasks/wp.php new file mode 100644 index 0000000..359e871 --- /dev/null +++ b/tasks/wp.php @@ -0,0 +1,114 @@ +desc('Download WordPress core files'); + +/** + * Push WordPress core files from local to remote + * + * Configuration: + * - wp/dir: Path to WordPress directory relative to document root + * - wp/filter: Rsync filter rules for WordPress core files (has defaults) + * + * Example: + * dep wp:push prod + */ +task('wp:push', function () { + $localWpDir = Localhost::getConfig('wp/dir'); + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => get("wp/filter"), + ]); + Files::pushFiles($localWpDir, '{{wp/dir}}', $rsyncOptions); +})->desc('Push WordPress core files from local to remote'); + +/** + * Pull WordPress core files from remote to local + * + * Configuration: + * - wp/dir: Path to WordPress directory relative to document root + * - wp/filter: Rsync filter rules for WordPress core files (has defaults) + * + * Example: + * dep wp:pull prod + */ +task('wp:pull', function () { + $localWpDir = Localhost::getConfig('wp/dir'); + $rsyncOptions = Rsync::buildOptionsArray([ + 'filter' => get("wp/filter"), + ]); + Files::pullFiles( '{{wp/dir}}', $localWpDir, $rsyncOptions); +})->desc('Pull WordPress core files from remote to local'); + +/** + * Display WP-CLI information + * + * Useful for debugging WP-CLI setup and configuration. + * Shows version, PHP info, and config paths. + * + * Configuration: + * - bin/wp: WP-CLI binary/command to use (automatically configured) + * + * Example: + * dep wp:info prod + */ +task('wp:info', function () { + WPCLI::runCommand("--info"); +})->desc('Display WP-CLI information and configuration'); + +/** + * Install WP-CLI on remote host + * + * Configuration (via CLI options): + * - installPath: Directory to install WP-CLI in (required) + * - binaryFile: Name of the WP-CLI binary (default: wp-cli.phar) + * - sudo: Whether to use sudo for installation (default: false) + * + * Example: + * dep wp:install-wpcli prod -o installPath=/usr/local/bin -o binaryFile=wp + * dep wp:install-wpcli prod -o installPath=/usr/local/bin -o sudo=true + */ +task('wp:install-wpcli', function () { + $installPath = get('installPath'); + if (empty($installPath)) { + throw new \RuntimeException( + 'You have to set an installPath for WordPress via a config variable or in cli via `-o installPath=$path`.' + ); + } + $binaryFile = get('binaryFile', 'wp-cli.phar'); + $sudo = get('sudo', false); + + WPCLI::install($installPath, $binaryFile, $sudo); +})->desc('Install WP-CLI on remote host'); From 9fbf29d505f134592fd698798c57290f4e161df1 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Sun, 23 Mar 2025 17:07:03 +0100 Subject: [PATCH 08/36] add test infrastructure and add tests for rsync util class --- .gitignore | 5 +- composer.json | 14 +- composer.lock | 2439 +++++++++++++++++++- phpunit.xml | 19 + src/Rsync.php | 8 +- tests/Fixtures/rsync/exclude.txt | 3 + tests/Fixtures/rsync/filter.txt | 3 + tests/Fixtures/rsync/include.txt | 3 + tests/Integration/IntegrationTestCase.php | 54 + tests/Integration/RsyncIntegrationTest.php | 182 ++ tests/Unit/RsyncTest.php | 150 ++ tests/Unit/UnitTestCase.php | 14 + tests/bootstrap.php | 21 + 13 files changed, 2900 insertions(+), 15 deletions(-) create mode 100644 phpunit.xml create mode 100644 tests/Fixtures/rsync/exclude.txt create mode 100644 tests/Fixtures/rsync/filter.txt create mode 100644 tests/Fixtures/rsync/include.txt create mode 100644 tests/Integration/IntegrationTestCase.php create mode 100644 tests/Integration/RsyncIntegrationTest.php create mode 100644 tests/Unit/RsyncTest.php create mode 100644 tests/Unit/UnitTestCase.php create mode 100644 tests/bootstrap.php diff --git a/.gitignore b/.gitignore index 864f912..8aca3aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /vendor -/tests /.cursor /.idea -.DS_STORE \ No newline at end of file +.DS_STORE +.phpunit.cache +.phpunit.result.cache \ No newline at end of file diff --git a/composer.json b/composer.json index e1ca9ce..9e92a8e 100644 --- a/composer.json +++ b/composer.json @@ -12,10 +12,11 @@ } ], "require-dev": { - "squizlabs/php_codesniffer": "^3.7" + "squizlabs/php_codesniffer": "^3.7", + "phpunit/phpunit": "^10.5" }, "require": { - "php": "^8.0|^7.3", + "php": "^8.1", "deployer/deployer": "^7.3" }, "autoload": { @@ -23,8 +24,15 @@ "Gaambo\\DeployerWordpress\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Gaambo\\DeployerWordpress\\Tests\\": "tests/", + "Deployer\\": "vendor/deployer/deployer/src/" + } + }, "scripts": { "phpcs": "phpcs", - "phpcs:fix": "phpcbf" + "phpcs:fix": "phpcbf", + "test": "phpunit" } } diff --git a/composer.lock b/composer.lock index 0283b91..da7c578 100644 --- a/composer.lock +++ b/composer.lock @@ -4,24 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c1d3afa8b7c8686c0065e6724d8ab35b", + "content-hash": "000b9ed04775d0ca321e628f9cbdcd53", "packages": [ { "name": "deployer/deployer", - "version": "v7.3.1", + "version": "v7.5.12", "source": { "type": "git", "url": "https://github.com/deployphp/deployer.git", - "reference": "c5c5e79d4e57445918ed24a9cdd3d85b0f261de3" + "reference": "efc71dac9ccc86b3f9946e75d50cb106b775aae2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/deployphp/deployer/zipball/c5c5e79d4e57445918ed24a9cdd3d85b0f261de3", - "reference": "c5c5e79d4e57445918ed24a9cdd3d85b0f261de3", + "url": "https://api.github.com/repos/deployphp/deployer/zipball/efc71dac9ccc86b3f9946e75d50cb106b775aae2", + "reference": "efc71dac9ccc86b3f9946e75d50cb106b775aae2", "shasum": "" }, + "require": { + "ext-json": "*", + "php": "^8.0|^7.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.64", + "pestphp/pest": "^3.3", + "phpstan/phpstan": "^1.4", + "phpunit/php-code-coverage": "^11.0", + "phpunit/phpunit": "^11.4" + }, "bin": [ - "dep" + "bin/dep" ], "type": "library", "notification-url": "https://packagist.org/downloads/", @@ -47,17 +58,2427 @@ "type": "github" } ], - "time": "2023-04-05T09:24:30+00:00" + "time": "2025-02-19T16:45:27+00:00" + } + ], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", + "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-02-12T12:17:51+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.4.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + }, + "time": "2024-12-30T11:07:19+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.45", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "bd68a781d8e30348bc297449f5234b3458267ae8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8", + "reference": "bd68a781d8e30348bc297449f5234b3458267ae8", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.3", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-02-06T16:08:12+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-18T14:56:07+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:17:12+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.12.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/2d1b63db139c3c6ea0c927698e5160f8b3b8d630", + "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-03-18T05:04:51+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T12:07:30+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.15" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:12+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" } ], - "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^8.0|^7.3" + "php": "^8.1" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..7f8c481 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,19 @@ + + + + + tests/Unit + + + tests/Integration + + + + + src + + + tests + + + diff --git a/src/Rsync.php b/src/Rsync.php index e72bc11..65c61b6 100644 --- a/src/Rsync.php +++ b/src/Rsync.php @@ -65,7 +65,7 @@ public static function buildFilter(array $filters = [], ?string $filterFile = nu foreach ($filters as $filter) { $filtersStrings[] = '--filter=' . $filter; } - if (!empty($filterFile)) { + if (!empty($filterFile) && file_exists($filterFile) && is_file($filterFile) && is_readable($filterFile)) { $filtersStrings[] = "--filter=merge " . $filterFile . ""; } if (!empty($filterPerDir)) { @@ -130,6 +130,12 @@ public static function buildOptionsArray(array $config = []): array $mergedConfig = array_merge($defaultConfig, $config); + // Filter out empty strings from arrays before building options + $mergedConfig['options'] = array_filter($mergedConfig['options']); + $mergedConfig['exclude'] = array_filter($mergedConfig['exclude']); + $mergedConfig['include'] = array_filter($mergedConfig['include']); + $mergedConfig['filter'] = array_filter($mergedConfig['filter']); + $options = array_merge( self::buildOptions($mergedConfig['options']), self::buildIncludes($mergedConfig['include'], $mergedConfig['include-file']), diff --git a/tests/Fixtures/rsync/exclude.txt b/tests/Fixtures/rsync/exclude.txt new file mode 100644 index 0000000..9dd43f3 --- /dev/null +++ b/tests/Fixtures/rsync/exclude.txt @@ -0,0 +1,3 @@ +*.log +*.tmp +.DS_Store \ No newline at end of file diff --git a/tests/Fixtures/rsync/filter.txt b/tests/Fixtures/rsync/filter.txt new file mode 100644 index 0000000..365dd27 --- /dev/null +++ b/tests/Fixtures/rsync/filter.txt @@ -0,0 +1,3 @@ ++ / ++ /wp-content/ +- /wp-content/uploads/* \ No newline at end of file diff --git a/tests/Fixtures/rsync/include.txt b/tests/Fixtures/rsync/include.txt new file mode 100644 index 0000000..b0f0249 --- /dev/null +++ b/tests/Fixtures/rsync/include.txt @@ -0,0 +1,3 @@ +*.php +*.js +*.css \ No newline at end of file diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php new file mode 100644 index 0000000..be22569 --- /dev/null +++ b/tests/Integration/IntegrationTestCase.php @@ -0,0 +1,54 @@ +deployer = new Deployer($console); + + // Create mocked Input and Output + $this->input = $this->createMock(Input::class); + $this->output = $this->createMock(Output::class); + + // Set up the mocked components in Deployer + $this->deployer['input'] = $this->input; + $this->deployer['output'] = $this->output; + + // Create a localhost instance for testing + $this->host = new Localhost(); + + // Push a new context with our host + Context::push(new Context($this->host)); + } + + protected function tearDown(): void + { + // Pop the context to clean up + Context::pop(); + + // Clean up the Deployer instance + unset($this->deployer); + + parent::tearDown(); + } +} \ No newline at end of file diff --git a/tests/Integration/RsyncIntegrationTest.php b/tests/Integration/RsyncIntegrationTest.php new file mode 100644 index 0000000..702cee8 --- /dev/null +++ b/tests/Integration/RsyncIntegrationTest.php @@ -0,0 +1,182 @@ +fixturesDir = __FIXTURES__ . '/rsync'; + + // Set default rsync configuration + $this->deployer->config->set('rsync', [ + 'exclude' => [ + '.git', + 'deploy.php', + ], + 'exclude-file' => false, + 'include' => [], + 'include-file' => false, + 'filter' => [], + 'filter-file' => false, + 'filter-perdir' => false, + 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterwards + ]); + } + + public function testBuildOptionsWithArray(): void + { + $options = ['delete-after', 'recursive', 'verbose']; + $result = Rsync::buildOptions($options); + + $this->assertCount(3, $result); + $this->assertContains('--delete-after', $result); + $this->assertContains('--recursive', $result); + $this->assertContains('--verbose', $result); + } + + public function testBuildOptionsWithEmptyArray(): void + { + $result = Rsync::buildOptions([]); + $this->assertEmpty($result); + } + + public function testBuildOptionsWithSpecialCharacters(): void + { + $options = ['delete after', 'recursive-update', 'verbose=true']; + $result = Rsync::buildOptions($options); + + $this->assertCount(3, $result); + $this->assertContains('--delete after', $result); + $this->assertContains('--recursive-update', $result); + $this->assertContains('--verbose=true', $result); + } + + public function testBuildOptionsArrayWithCustomConfig(): void + { + $config = [ + 'exclude' => ['*.log', '*.tmp'], + 'exclude-file' => $this->fixturesDir . '/exclude.txt', + 'include' => ['*.php', '*.js'], + 'include-file' => $this->fixturesDir . '/include.txt', + 'filter' => ['+ /wp-content/'], + 'filter-file' => $this->fixturesDir . '/filter.txt', + 'filter-perdir' => '.deployfilter', + 'options' => ['delete-after', 'recursive'] + ]; + + $result = Rsync::buildOptionsArray($config); + + $this->assertContains('--exclude=*.log', $result); + $this->assertContains('--exclude=*.tmp', $result); + $this->assertContains('--exclude-from=' . $config['exclude-file'], $result); + $this->assertContains('--include=*.php', $result); + $this->assertContains('--include=*.js', $result); + $this->assertContains('--include-from=' . $config['include-file'], $result); + $this->assertContains('--filter=+ /wp-content/', $result); + $this->assertContains('--filter=merge ' . $config['filter-file'], $result); + $this->assertContains('--filter=dir-merge ' . $config['filter-perdir'], $result); + $this->assertContains('--delete-after', $result); + $this->assertContains('--recursive', $result); + } + + public function testBuildOptionsArrayWithDefaults(): void + { + $result = Rsync::buildOptionsArray(); + + // Default config should include these + $this->assertContains('--exclude=.git', $result); + $this->assertContains('--exclude=deploy.php', $result); + $this->assertContains('--delete-after', $result); + + // These should not be present in default config + $this->assertNotContains('--include=', $result); + $this->assertNotContains('--include-from=', $result); + $this->assertNotContains('--filter=', $result); + $this->assertNotContains('--filter=merge', $result); + $this->assertNotContains('--filter=dir-merge', $result); + } + + public function testBuildOptionsArrayWithEmptyConfig(): void + { + $result = Rsync::buildOptionsArray([]); + + // Should still include defaults + $this->assertContains('--exclude=.git', $result); + $this->assertContains('--exclude=deploy.php', $result); + $this->assertContains('--delete-after', $result); + } + + public function testBuildOptionsArrayWithNoDeployerConfig(): void + { + // Remove the rsync config + $this->deployer->config->set('rsync', null); + + $result = Rsync::buildOptionsArray(); + + // Should use hardcoded defaults + $this->assertContains('--exclude=.git', $result); + $this->assertContains('--exclude=deploy.php', $result); + $this->assertContains('--delete-after', $result); + } + + public function testBuildOptionsArrayWithInvalidDeployerConfig(): void + { + // Set an invalid config + $this->deployer->config->set('rsync', 'invalid'); + + $result = Rsync::buildOptionsArray(); + + // Should use hardcoded defaults + $this->assertContains('--exclude=.git', $result); + $this->assertContains('--exclude=deploy.php', $result); + $this->assertContains('--delete-after', $result); + } + + public function testBuildOptionsArrayWithInvalidFilePaths(): void + { + $config = [ + 'exclude-file' => '/nonexistent/exclude.txt', + 'include-file' => '/nonexistent/include.txt', + 'filter-file' => '/nonexistent/filter.txt' + ]; + + $result = Rsync::buildOptionsArray($config); + + // Invalid files should be ignored + $this->assertNotContains('--exclude-from=/nonexistent/exclude.txt', $result); + $this->assertNotContains('--include-from=/nonexistent/include.txt', $result); + $this->assertNotContains('--filter=merge /nonexistent/filter.txt', $result); + } + + public function testBuildOptionsArrayFiltersEmptyStrings(): void + { + $config = [ + 'options' => ['', 'delete-after', '', 'recursive', ''], + 'exclude' => ['', '.git', ''], + 'include' => ['', '*.php', ''], + 'filter' => ['', '+ /wp-content/', ''] + ]; + + $result = Rsync::buildOptionsArray($config); + + // Check that non-empty values are present + $this->assertContains('--delete-after', $result); + $this->assertContains('--recursive', $result); + $this->assertContains('--exclude=.git', $result); + $this->assertContains('--include=*.php', $result); + $this->assertContains('--filter=+ /wp-content/', $result); + + // Check that no empty values are present + $this->assertNotContains('', $result, "Empty string found in result"); + $this->assertNotContains('--exclude=', $result, "Empty exclude found in result"); + $this->assertNotContains('--include=', $result, "Empty include found in result"); + $this->assertNotContains('--filter=', $result, "Empty filter found in result"); + } +} \ No newline at end of file diff --git a/tests/Unit/RsyncTest.php b/tests/Unit/RsyncTest.php new file mode 100644 index 0000000..ad559ee --- /dev/null +++ b/tests/Unit/RsyncTest.php @@ -0,0 +1,150 @@ +fixturesDir = __FIXTURES__ . '/rsync'; + } + + public function testBuildExcludesWithExistingFile() + { + $excludes = ['*.log', '*.tmp', '.DS_Store']; + $excludeFile = $this->fixturesDir . '/exclude.txt'; + + $result = Rsync::buildExcludes($excludes, $excludeFile); + + $this->assertContains('--exclude=*.log', $result); + $this->assertContains('--exclude=*.tmp', $result); + $this->assertContains('--exclude=.DS_Store', $result); + $this->assertContains('--exclude-from=' . $excludeFile, $result); + } + + public function testBuildExcludesWithNonExistingFile() + { + $excludes = ['*.log', '*.tmp', '.DS_Store']; + $excludeFile = '/path/to/nonexistent.txt'; + + $result = Rsync::buildExcludes($excludes, $excludeFile); + + $this->assertContains('--exclude=*.log', $result); + $this->assertContains('--exclude=*.tmp', $result); + $this->assertContains('--exclude=.DS_Store', $result); + $this->assertNotContains('--exclude-from=' . $excludeFile, $result); + } + + public function testBuildIncludesWithExistingFile() + { + $includes = ['*.php', '*.js', '*.css']; + $includeFile = $this->fixturesDir . '/include.txt'; + + $result = Rsync::buildIncludes($includes, $includeFile); + + $this->assertContains('--include=*.php', $result); + $this->assertContains('--include=*.js', $result); + $this->assertContains('--include=*.css', $result); + $this->assertContains('--include-from=' . $includeFile, $result); + } + + public function testBuildIncludesWithNonExistingFile() + { + $includes = ['*.php', '*.js', '*.css']; + $includeFile = '/path/to/nonexistent.txt'; + + $result = Rsync::buildIncludes($includes, $includeFile); + + $this->assertContains('--include=*.php', $result); + $this->assertContains('--include=*.js', $result); + $this->assertContains('--include=*.css', $result); + $this->assertNotContains('--include-from=' . $includeFile, $result); + } + + public function testBuildFilterWithExistingFile() + { + $filters = ['+ /wp-content/', '- /wp-content/uploads/*']; + $filterFile = $this->fixturesDir . '/filter.txt'; + $filterPerDir = '.deployfilter'; + + $result = Rsync::buildFilter($filters, $filterFile, $filterPerDir); + + $this->assertContains('--filter=+ /wp-content/', $result); + $this->assertContains('--filter=- /wp-content/uploads/*', $result); + $this->assertContains('--filter=merge ' . $filterFile, $result); + $this->assertContains('--filter=dir-merge ' . $filterPerDir, $result); + } + + public function testBuildFilterWithNonExistingFile() + { + $filters = ['+ /wp-content/', '- /wp-content/uploads/*']; + $filterFile = '/path/to/nonexistent.txt'; + $filterPerDir = '.deployfilter'; + + $result = Rsync::buildFilter($filters, $filterFile, $filterPerDir); + + $this->assertContains('--filter=+ /wp-content/', $result); + $this->assertContains('--filter=- /wp-content/uploads/*', $result); + $this->assertNotContains('--filter=merge ' . $filterFile, $result); + $this->assertContains('--filter=dir-merge ' . $filterPerDir, $result); + } + + public function testBuildExcludesWithArray(): void + { + $excludes = ['*.log', '*.tmp', '.DS_Store']; + $result = Rsync::buildExcludes($excludes); + + $this->assertCount(3, $result); + $this->assertContains('--exclude=*.log', $result); + $this->assertContains('--exclude=*.tmp', $result); + $this->assertContains('--exclude=.DS_Store', $result); + } + + public function testBuildExcludesWithEmptyArray(): void + { + $result = Rsync::buildExcludes([]); + $this->assertEmpty($result); + } + + public function testBuildIncludesWithArray(): void + { + $includes = ['*.php', '*.js', '*.css']; + $result = Rsync::buildIncludes($includes); + + $this->assertCount(3, $result); + $this->assertContains('--include=*.php', $result); + $this->assertContains('--include=*.js', $result); + $this->assertContains('--include=*.css', $result); + } + + public function testBuildIncludesWithEmptyArray(): void + { + $result = Rsync::buildIncludes([]); + $this->assertEmpty($result); + } + + public function testBuildFilterWithArray(): void + { + $filters = [ + '+ /wp-content/', + '- /wp-content/uploads/*' + ]; + $result = Rsync::buildFilter($filters); + + $this->assertCount(2, $result); + $this->assertContains('--filter=+ /wp-content/', $result); + $this->assertContains('--filter=- /wp-content/uploads/*', $result); + } + + public function testBuildFilterWithEmptyArray(): void + { + $result = Rsync::buildFilter([]); + $this->assertEmpty($result); + } +} \ No newline at end of file diff --git a/tests/Unit/UnitTestCase.php b/tests/Unit/UnitTestCase.php new file mode 100644 index 0000000..6a7e4b3 --- /dev/null +++ b/tests/Unit/UnitTestCase.php @@ -0,0 +1,14 @@ + Date: Mon, 24 Mar 2025 10:52:59 +0100 Subject: [PATCH 09/36] add code quality tools and tasks, and fix current errors --- .editorconfig | 18 + .gitattributes | 9 + .github/workflows/code-quality.yml | 55 ++ .github/workflows/composer-diff.yml | 32 ++ .github/workflows/pull-request.yml | 36 ++ composer.json | 7 +- composer.lock | 761 +++------------------------- examples/bedrock/deploy.php | 11 +- examples/simple/deploy.php | 11 +- phpstan.neon | 9 + recipes/bedrock.php | 7 +- recipes/common.php | 11 +- src/Composer.php | 35 +- src/Files.php | 7 +- src/Localhost.php | 5 +- src/NPM.php | 36 +- src/Rsync.php | 175 +++---- src/Utils.php | 6 +- src/WPCLI.php | 17 +- tasks/wp.php | 2 +- tests/bootstrap.php | 3 +- 21 files changed, 398 insertions(+), 855 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/code-quality.yml create mode 100644 .github/workflows/composer-diff.yml create mode 100644 .github/workflows/pull-request.yml create mode 100644 phpstan.neon diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ea6ed34 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{json,yml,yaml,md}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.php] +max_line_length = 120 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9947fee --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github/ export-ignore +/.gitignore export-ignore +/docs/ export-ignore +/phpcs.xml export-ignore +/phpstan.neon export-ignore +/phpunit.xml export-ignore +/tests/ export-ignore \ No newline at end of file diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..9334055 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,55 @@ +name: Code Quality + +on: + push: + pull_request: + # Allow manually triggering the workflow. + workflow_dispatch: + +jobs: + code-quality: + name: Run Code Quality Checks + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + php: ['8.1', '8.2', '8.3', '8.4'] + fail-fast: false + + steps: + # Checkout the repository + - name: Checkout repository + uses: actions/checkout@v4 + + # Setup PHP with Composer and cs2pr + - name: Setup PHP and tools + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2, cs2pr + + # Validate composer.json + - name: Validate composer.json + run: composer validate --strict + + # Install dependencies with caching + - name: Install Composer dependencies + uses: ramsey/composer-install@v3 + with: + composer-options: '--prefer-dist --no-progress' + + # Run PHP syntax linting + - name: Run PHP syntax linting + run: composer run-script lint + + # Run PHP CodeSniffer with GitHub annotations + - name: Run PHP CodeSniffer + run: | + composer run-script phpcs -- --standard=phpcs.xml --report-checkstyle=./phpcs-report.xml + cs2pr ./phpcs-report.xml + + # Run PHPStan with GitHub annotations + - name: Run PHPStan + run: | + composer run-script phpstan -- --error-format=checkstyle > phpstan-report.xml + cs2pr ./phpstan-report.xml \ No newline at end of file diff --git a/.github/workflows/composer-diff.yml b/.github/workflows/composer-diff.yml new file mode 100644 index 0000000..9d7a9bf --- /dev/null +++ b/.github/workflows/composer-diff.yml @@ -0,0 +1,32 @@ +name: Composer Lock Diff + +on: + pull_request: + paths: + - 'composer.json' + - 'composer.lock' + +jobs: + composer-diff: + name: Composer Lock Diff + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + # Checkout the repository + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Generate composer.lock diff + - name: Generate composer.lock diff + id: composer-diff + uses: IonBazan/composer-diff-action@v1 + + # Post diff as sticky comment + - name: Post diff as sticky comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + message: | + ## Composer Package Changes + ${{ steps.composer-diff.outputs.diff }} \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..539b2da --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,36 @@ +name: Pull Request Tests + +on: + pull_request: + +jobs: + tests: + name: Run PHPUnit Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + php: ['8.1', '8.2', '8.3', '8.4'] + fail-fast: false + + steps: + # Checkout the repository + - name: Checkout repository + uses: actions/checkout@v4 + + # Setup PHP with Composer + - name: Setup PHP and tools + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + + # Install dependencies with caching + - name: Install Composer dependencies + uses: ramsey/composer-install@v3 + with: + composer-options: '--prefer-dist --no-progress' + + # Run PHPUnit tests + - name: Run PHPUnit tests + run: composer run-script test \ No newline at end of file diff --git a/composer.json b/composer.json index 9e92a8e..8b0ec4f 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,9 @@ } ], "require-dev": { - "squizlabs/php_codesniffer": "^3.7", - "phpunit/phpunit": "^10.5" + "squizlabs/php_codesniffer": "^3.11", + "phpunit/phpunit": "^10.5", + "phpstan/phpstan": "^2.1" }, "require": { "php": "^8.1", @@ -33,6 +34,8 @@ "scripts": { "phpcs": "phpcs", "phpcs:fix": "phpcbf", + "phpstan": "phpstan analyse", + "lint": "find . -type f -name '*.php' -not -path './vendor/*' -exec php -l {} \\;", "test": "phpunit" } } diff --git a/composer.lock b/composer.lock index da7c578..0f6e74f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "000b9ed04775d0ca321e628f9cbdcd53", + "content-hash": "e131dd274fe0412de80fe645a5b2e1fe", "packages": [ { "name": "deployer/deployer", @@ -298,6 +298,64 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpstan/phpstan", + "version": "2.1.10", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "051a3b6b9b80df4ba3a7f801a8b53ad7d8f1c15f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/051a3b6b9b80df4ba3a7f801a8b53ad7d8f1c15f", + "reference": "051a3b6b9b80df4ba3a7f801a8b53ad7d8f1c15f", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-03-23T14:57:55+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "10.1.16", @@ -720,59 +778,6 @@ ], "time": "2025-02-06T16:08:12+00:00" }, - { - "name": "psr/container", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" - }, { "name": "sebastian/cli-parser", "version": "2.0.1", @@ -1773,654 +1778,6 @@ ], "time": "2025-03-18T05:04:51+00:00" }, - { - "name": "symfony/console", - "version": "v6.4.17", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", - "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" - }, - "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v6.4.17" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-12-07T12:07:30+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.5-dev" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.5-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" - }, - { - "name": "symfony/string", - "version": "v6.4.15", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/translation-contracts": "<2.5" - }, - "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/intl": "^6.2|^7.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], - "support": { - "source": "https://github.com/symfony/string/tree/v6.4.15" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-11-13T13:31:12+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.3", diff --git a/examples/bedrock/deploy.php b/examples/bedrock/deploy.php index 3e28f81..36e7bd1 100644 --- a/examples/bedrock/deploy.php +++ b/examples/bedrock/deploy.php @@ -1,12 +1,17 @@ set('current_path', function () { - return getLocalhost()->get('release_path'); + return Localhost::getConfig('release_path'); }) // Bedrock dirs ->set('uploads/dir', 'web/app/uploads') @@ -46,7 +51,7 @@ // Build package assets via npm locally task('deploy:build_assets', function () { - on(getLocalhost(), function () { + on(Localhost::get(), function () { if (has('packages')) { // Do not install vendors on each deployment. // invoke('packages:assets:vendors'); diff --git a/examples/simple/deploy.php b/examples/simple/deploy.php index dfae362..4fce2bf 100644 --- a/examples/simple/deploy.php +++ b/examples/simple/deploy.php @@ -1,12 +1,17 @@ set('current_path', function () { - return getLocalhost()->get('release_path'); + return Localhost::getConfig('release_path'); }) ->set('dump_path', __DIR__ . '/data/db_dumps') ->set('backup_path', __DIR__ . '/data/backups'); @@ -39,7 +44,7 @@ // Build package assets via npm locally task('deploy:build_assets', function () { - on(getLocalhost(), function () { + on(Localhost::get(), function () { if (has('packages')) { // Do not install vendors on each deployment. // invoke('packages:assets:vendors'); diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..44b8b61 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +parameters: + level: 5 + paths: + - src + - recipes + - examples + - tasks + scanDirectories: + - vendor/deployer/deployer \ No newline at end of file diff --git a/recipes/bedrock.php b/recipes/bedrock.php index 039da78..bdbe37b 100644 --- a/recipes/bedrock.php +++ b/recipes/bedrock.php @@ -8,14 +8,17 @@ namespace Gaambo\DeployerWordpress\Recipes\Bedrock; +use Gaambo\DeployerWordpress\Composer; +use Gaambo\DeployerWordpress\Rsync; + use function Deployer\add; +use function Deployer\after; use function Deployer\get; use function Deployer\run; use function Deployer\set; use function Deployer\task; use function Deployer\test; -use Gaambo\DeployerWordpress\Composer; -use Gaambo\DeployerWordpress\Rsync; +use function Deployer\upload; require_once __DIR__ . '/common.php'; diff --git a/recipes/common.php b/recipes/common.php index e292a72..35d81df 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -8,10 +8,14 @@ namespace Gaambo\DeployerWordpress\Recipes\Common; use Deployer\Deployer; +use Deployer\Host\Host; +use Gaambo\DeployerWordpress\Composer; +use Gaambo\DeployerWordpress\Localhost; +use Gaambo\DeployerWordpress\WPCLI; -use function Deployer\add; use function Deployer\after; use function Deployer\commandExist; +use function Deployer\currentHost; use function Deployer\get; use function Deployer\has; use function Deployer\info; @@ -25,9 +29,6 @@ use function Deployer\test; use function Deployer\warning; use function Deployer\which; -use Gaambo\DeployerWordpress\Composer; -use Gaambo\DeployerWordpress\Localhost; -use Gaambo\DeployerWordpress\WPCLI; $deployerPath = 'vendor/deployer/deployer/'; require_once $deployerPath . 'recipe/common.php'; @@ -203,7 +204,7 @@ // Overwrite deploy:info task to show host instead of branch task('deploy:info', function () { $selectedHosts = selectedHosts(); - $hosts = implode(',', array_map(function (\Deployer\Host\Host $host) { + $hosts = implode(',', array_map(function (Host $host) { return $host->getAlias(); }, $selectedHosts)); info("deploying to $hosts"); diff --git a/src/Composer.php b/src/Composer.php index 2c75241..ddf56e3 100644 --- a/src/Composer.php +++ b/src/Composer.php @@ -21,54 +21,55 @@ public static function runDefault(string $path): string } /** - * Run a composer script + * Run any composer command with verbosity flags * Uses Deployers run function * - * @param string $path Path in which to run script - * @param string $script Script-name defined in composer.json to be run + * @param string $path Path in which to run composer command + * @param string $command Composer command to run * @param string $arguments Command-line arguments to be passed to composer * @return string Output of the command */ - public static function runScript(string $path, string $script, string $arguments = ''): string + public static function runCommand(string $path, string $command, string $arguments = ''): string { - return self::runCommand($path, "run-script $script", $arguments); + return run("cd $path && {{bin/composer}} $command $arguments " . Utils::getVerbosityArgument()); } /** - * Run any composer command with verbosity flags + * Run a composer script * Uses Deployers run function * - * @param string $path Path in which to run composer command - * @param string $command Composer command to run + * @param string $path Path in which to run script + * @param string $script Script-name defined in composer.json to be run * @param string $arguments Command-line arguments to be passed to composer * @return string Output of the command */ - public static function runCommand(string $path, string $command, string $arguments = ''): string + public static function runScript(string $path, string $script, string $arguments = ''): string { - return run("cd $path && {{bin/composer}} $command $arguments " . Utils::getVerbosityArgument()); + return self::runCommand($path, "run-script $script", $arguments); } /** - * Install the composer binary - * Uses Deployers run function - * + * Install composer binary * @param string $installPath Path where to install the binary * @param string $binaryName Name for the binary file * @param bool $sudo Whether to use sudo for moving the binary * @return string Path to installed and moved binary file */ - public static function install(string $installPath, string $binaryName = 'composer.phar', bool $sudo = false): string - { + public static function install( + string $installPath, + string $binaryName = 'composer.phar', + bool $sudo = false + ): string { $sudoCommand = $sudo ? 'sudo ' : ''; run("mkdir -p $installPath"); run("cd $installPath && curl -sS " . self::INSTALLER_DOWNLOAD . " | {{bin/php}}"); run('mv {{deploy_path}}/composer.phar {{deploy_path}}/.dep/composer.phar'); - + if ($binaryName !== 'composer.phar') { run("$sudoCommand mv $installPath/composer.phar $installPath/$binaryName"); } return "$installPath/$binaryName"; } -} \ No newline at end of file +} diff --git a/src/Files.php b/src/Files.php index 9d6c3b2..50a656d 100644 --- a/src/Files.php +++ b/src/Files.php @@ -2,9 +2,9 @@ namespace Gaambo\DeployerWordpress; -use function Deployer\upload; use function Deployer\download; use function Deployer\run; +use function Deployer\upload; class Files { @@ -38,7 +38,8 @@ public static function pullFiles(string $remotePath, string $localPath, array $r * Zip files into a backup zip * * @param string $dir Directory to zip - * Can have a trailing slash, which backups the contents of the directory, if not it backups the directory into the zip + * Can have a trailing slash, which backups the contents of the directory, + * if not it backups the directory into the zip * @param string $backupDir Directory in which to store the zip * @param string $filename Filename of the zip file - gets prefixed to a datetime * @return string The full path ($backupDir + full filename) to the created zip @@ -62,4 +63,4 @@ public static function zipFiles(string $dir, string $backupDir, string $filename return $backupPath; } -} \ No newline at end of file +} diff --git a/src/Localhost.php b/src/Localhost.php index 2d98577..0b1c01e 100644 --- a/src/Localhost.php +++ b/src/Localhost.php @@ -2,6 +2,7 @@ namespace Gaambo\DeployerWordpress; +use Deployer\Deployer; use Deployer\Host\Host; /** @@ -27,6 +28,6 @@ public static function getConfig(string $key): mixed */ public static function get(): Host { - return \Deployer\Deployer::get()->hosts->get('localhost'); + return Deployer::get()->hosts->get('localhost'); } -} \ No newline at end of file +} diff --git a/src/NPM.php b/src/NPM.php index 2ce9a44..a3ac3bc 100644 --- a/src/NPM.php +++ b/src/NPM.php @@ -2,8 +2,8 @@ namespace Gaambo\DeployerWordpress; -use function Deployer\run; use function Deployer\has; +use function Deployer\run; use function Deployer\test; class NPM @@ -22,6 +22,22 @@ public static function runScript(string $path, string $script, string $arguments return self::runCommand($path, "run-script $script", $arguments); } + /** + * Run any npm command + * Uses Deployers run function + * + * @param string $path Path in which to run npm command + * @param string $action NPM action to be run + * @param string $arguments Command-line arguments to be passed to npm + * @return string Output of the command + */ + public static function runCommand(string $path, string $action, string $arguments = ''): string + { + $verbosityArgument = Utils::getVerbosityArgument(); + $verbosityArgument = str_replace('v', 'd', $verbosityArgument); // npm takes d for verbosity argument + return run("cd $path && {{bin/npm}} $action $arguments $verbosityArgument"); + } + /** * Run npm install * Tries to copy node_modules from previous release if available @@ -39,20 +55,4 @@ public static function runInstall(string $path, string $arguments = ''): string } return self::runCommand($path, 'install', $arguments); } - - /** - * Run any npm command - * Uses Deployers run function - * - * @param string $path Path in which to run npm command - * @param string $action NPM action to be run - * @param string $arguments Command-line arguments to be passed to npm - * @return string Output of the command - */ - public static function runCommand(string $path, string $action, string $arguments = ''): string - { - $verbosityArgument = Utils::getVerbosityArgument(); - $verbosityArgument = str_replace('v', 'd', $verbosityArgument); // npm takes d for verbosity argument - return run("cd $path && {{bin/npm}} $action $arguments $verbosityArgument"); - } -} \ No newline at end of file +} diff --git a/src/Rsync.php b/src/Rsync.php index 65c61b6..8311037 100644 --- a/src/Rsync.php +++ b/src/Rsync.php @@ -6,91 +6,6 @@ class Rsync { - /** - * Build excludes command-line arguments from array/file - * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options - * - * @param array $excludes Array of paths/files to exclude - * @param string|null $excludeFile Path to a exclude file to pass to rsync - * @return array Excludes as command-line arguments which can be passed to rsync - */ - public static function buildExcludes(array $excludes = [], ?string $excludeFile = null): array - { - $excludesStrings = []; - foreach ($excludes as $exclude) { - $excludesStrings[] = '--exclude=' . $exclude; - } - - if (!empty($excludeFile) && file_exists($excludeFile) && is_file($excludeFile) && is_readable($excludeFile)) { - $excludesStrings[] = '--exclude-from=' . $excludeFile; - } - - return $excludesStrings; - } - - /** - * Build includes command-line arguments from array/file - * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options - * - * @param array $includes Array of paths/files to include - * @param string|null $includeFile Path to a include file to pass to rsync - * @return array Includes as command-line arguments which can be passed to rsync - */ - public static function buildIncludes(array $includes = [], ?string $includeFile = null): array - { - $includesStrings = []; - foreach ($includes as $include) { - $includesStrings[] = '--include=' . $include; - } - - if (!empty($includeFile) && file_exists($includeFile) && is_file($includeFile) && is_readable($includeFile)) { - $includesStrings[] .= '--include-from=' . $includeFile; - } - - return $includesStrings; - } - - /** - * Build filter command-line arguments from array/file/filePerDir - * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options - * - * @param array $filters Array of rsync filters - * @param string|null $filterFile Path to a filterFile in rsync filter syntax - * @param string|null $filterPerDir Filename to be used on a per directory basis for rsync filtering - * @return array Filters as command-line arguments which can be passed to rsync - */ - public static function buildFilter(array $filters = [], ?string $filterFile = null, ?string $filterPerDir = null): array - { - $filtersStrings = []; - foreach ($filters as $filter) { - $filtersStrings[] = '--filter=' . $filter; - } - if (!empty($filterFile) && file_exists($filterFile) && is_file($filterFile) && is_readable($filterFile)) { - $filtersStrings[] = "--filter=merge " . $filterFile . ""; - } - if (!empty($filterPerDir)) { - $filtersStrings[] = "--filter=dir-merge " . $filterPerDir . ""; - } - return $filtersStrings; - } - - /** - * Build rsync options and flags - * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options - * - * @param array $options Array of options/flags to be passed to rsync - do not include dashes! - * @return array Options as command-line arguments which can be passed to rsync - */ - public static function buildOptions(array $options): array - { - $optionsStrings = []; - foreach ($options as $option) { - $optionsStrings[] = '--' . $option; - } - - return $optionsStrings; - } - /** * Build rsync options array * Takes a full config array and builds the different command-line arguments to be passed to rsync @@ -147,4 +62,92 @@ public static function buildOptionsArray(array $config = []): array // because Rsync class uses escapeshellarg return array_filter($options); } -} \ No newline at end of file + + /** + * Build rsync options and flags + * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options + * + * @param array $options Array of options/flags to be passed to rsync - do not include dashes! + * @return array Options as command-line arguments which can be passed to rsync + */ + public static function buildOptions(array $options): array + { + $optionsStrings = []; + foreach ($options as $option) { + $optionsStrings[] = '--' . $option; + } + + return $optionsStrings; + } + + /** + * Build includes command-line arguments from array/file + * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options + * + * @param array $includes Array of paths/files to include + * @param string|null $includeFile Path to a include file to pass to rsync + * @return array Includes as command-line arguments which can be passed to rsync + */ + public static function buildIncludes(array $includes = [], ?string $includeFile = null): array + { + $includesStrings = []; + foreach ($includes as $include) { + $includesStrings[] = '--include=' . $include; + } + + if (!empty($includeFile) && file_exists($includeFile) && is_file($includeFile) && is_readable($includeFile)) { + $includesStrings[] = '--include-from=' . $includeFile; + } + + return $includesStrings; + } + + /** + * Build excludes command-line arguments from array/file + * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options + * + * @param array $excludes Array of paths/files to exclude + * @param string|null $excludeFile Path to a exclude file to pass to rsync + * @return array Excludes as command-line arguments which can be passed to rsync + */ + public static function buildExcludes(array $excludes = [], ?string $excludeFile = null): array + { + $excludesStrings = []; + foreach ($excludes as $exclude) { + $excludesStrings[] = '--exclude=' . $exclude; + } + + if (!empty($excludeFile) && file_exists($excludeFile) && is_file($excludeFile) && is_readable($excludeFile)) { + $excludesStrings[] = '--exclude-from=' . $excludeFile; + } + + return $excludesStrings; + } + + /** + * Build filter command-line arguments from array/file/filePerDir + * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options + * + * @param array $filters Array of rsync filters + * @param string|null $filterFile Path to a filterFile in rsync filter syntax + * @param string|null $filterPerDir Filename to be used on a per directory basis for rsync filtering + * @return array Filters as command-line arguments which can be passed to rsync + */ + public static function buildFilter( + array $filters = [], + ?string $filterFile = null, + ?string $filterPerDir = null + ): array { + $filtersStrings = []; + foreach ($filters as $filter) { + $filtersStrings[] = '--filter=' . $filter; + } + if (!empty($filterFile) && file_exists($filterFile) && is_file($filterFile) && is_readable($filterFile)) { + $filtersStrings[] = "--filter=merge " . $filterFile . ""; + } + if (!empty($filterPerDir)) { + $filtersStrings[] = "--filter=dir-merge " . $filterPerDir . ""; + } + return $filtersStrings; + } +} diff --git a/src/Utils.php b/src/Utils.php index c4311f6..88b97ee 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -15,7 +15,7 @@ public static function getVerbosityArgument(): string { $outputInterface = output(); $verbosityArgument = ''; - + if ($outputInterface->isVerbose()) { $verbosityArgument = '-v'; } @@ -25,7 +25,7 @@ public static function getVerbosityArgument(): string if ($outputInterface->isDebug()) { $verbosityArgument = '-vvv'; } - + return $verbosityArgument; } -} \ No newline at end of file +} diff --git a/src/WPCLI.php b/src/WPCLI.php index e95cbde..8fafec1 100644 --- a/src/WPCLI.php +++ b/src/WPCLI.php @@ -4,7 +4,6 @@ use function Deployer\run; use function Deployer\runLocally; -use function Deployer\test; /** * WP CLI utility class @@ -13,6 +12,7 @@ class WPCLI { private const INSTALLER_DOWNLOAD = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar'; + private const DEFAULT_PATH = '{{release_or_current_path}}'; /** * Run a WP CLI command @@ -21,7 +21,7 @@ class WPCLI * @param string $arguments Additional arguments to pass to WP-CLI * @return void */ - public static function runCommand(string $command, ?string $path = '{{release_or_current_path}}', string $arguments = ''): void + public static function runCommand(string $command, ?string $path = self::DEFAULT_PATH, string $arguments = ''): void { $cmd = "{{bin/wp}} $command $arguments"; if ($path) { @@ -38,8 +38,11 @@ public static function runCommand(string $command, ?string $path = '{{release_or * @param string $arguments Additional arguments to pass to WP-CLI * @return void */ - public static function runCommandLocally(string $command, ?string $path = '{{release_or_current_path}}', string $arguments = ''): void - { + public static function runCommandLocally( + string $command, + ?string $path = self::DEFAULT_PATH, + string $arguments = '' + ): void { $localWp = Localhost::getConfig('bin/wp'); if ($path) { runLocally("cd $path && $localWp $command $arguments"); @@ -63,11 +66,11 @@ public static function install(string $installPath, string $binaryName = 'wp-cli run("mkdir -p $installPath"); run("cd $installPath && curl -sS -O " . self::INSTALLER_DOWNLOAD); - + if ($binaryName !== 'wp-cli.phar') { - run("$sudoCommand mv $installPath/wp-cli.phar $installPath/$binaryName"); + run("$sudoPrefix mv $installPath/wp-cli.phar $installPath/$binaryName"); } return "$installPath/$binaryName"; } -} \ No newline at end of file +} diff --git a/tasks/wp.php b/tasks/wp.php index 359e871..a4e3d00 100644 --- a/tasks/wp.php +++ b/tasks/wp.php @@ -33,7 +33,7 @@ * dep wp:download-core production -o wp/version=6.4.3 */ task('wp:download-core', function () { - WPCLI::runCommand("core download --skip-content --version= $wpVersion"); + WPCLI::runCommand("core download --skip-content --version=" . get('wp/version', 'latest')); })->desc('Download WordPress core files'); /** diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 912d43a..1f24445 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -5,8 +5,9 @@ // Somehow deployer removes its autoload after releasing. // see https://github.com/deployphp/deployer/commit/76fadcd887eb22e37ffe92c5e05964ce43c9cfe5 // And also add Deployer PSR-4 autoload-dev to composer.json -require __DIR__ . '/../vendor/deployer/deployer/src/functions.php'; +// These are automatically loaded by Deployer when running the dep binary. require __DIR__ . '/../vendor/deployer/deployer/src/Support/helpers.php'; +require __DIR__ . '/../vendor/deployer/deployer/src/functions.php'; set_include_path(__DIR__ . '/../vendor/deployer/deployer' . PATH_SEPARATOR . get_include_path()); From 3e9a87cf4ba9e242c27428b33077a32df67ab15f Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 10:58:49 +0100 Subject: [PATCH 10/36] remove composer version to make composer.json valid --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 8b0ec4f..8389055 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,6 @@ { "name": "gaambo/deployer-wordpress", "description": "Deployer tasks for deploying WordPress Sites", - "version": "3.1.0", "type": "library", "license": "MIT", "homepage": "https://github.com/gaambo/deployer-wordpress", From 3d1bd7b96486a476849f402cb869162a2a94129b Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 10:59:38 +0100 Subject: [PATCH 11/36] add precommit script to run all code quality tasks locally and fix tests --- composer.json | 17 +- composer.lock | 703 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 716 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 8389055..53251d3 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,10 @@ } ], "require-dev": { - "squizlabs/php_codesniffer": "^3.11", + "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^10.5", - "phpstan/phpstan": "^2.1" + "squizlabs/php_codesniffer": "^3.11", + "symfony/console": "^6.4" }, "require": { "php": "^8.1", @@ -35,6 +36,16 @@ "phpcs:fix": "phpcbf", "phpstan": "phpstan analyse", "lint": "find . -type f -name '*.php' -not -path './vendor/*' -exec php -l {} \\;", - "test": "phpunit" + "test": "phpunit", + "precommit": [ + "@lint", + "@phpcs", + "@phpstan", + "@composer validate", + "@test" + ] + }, + "config": { + "sort-packages": true } } diff --git a/composer.lock b/composer.lock index 0f6e74f..fe60991 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e131dd274fe0412de80fe645a5b2e1fe", + "content-hash": "1df277ea9805fbaebf8f6b3bce85b59e", "packages": [ { "name": "deployer/deployer", @@ -778,6 +778,59 @@ ], "time": "2025-02-06T16:08:12+00:00" }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, { "name": "sebastian/cli-parser", "version": "2.0.1", @@ -1778,6 +1831,654 @@ ], "time": "2025-03-18T05:04:51+00:00" }, + { + "name": "symfony/console", + "version": "v6.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T12:07:30+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.15" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:12+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.3", From 35c7e00ebf4f535d9b3c32867517cbb13cdcd7ed Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 12:19:56 +0100 Subject: [PATCH 12/36] more code style/quality fixes --- composer.json | 8 +- examples/bedrock/deploy.php | 2 +- examples/simple/deploy.php | 2 +- phpcs.xml | 12 +- phpstan.neon | 6 +- recipes/bedrock.php | 2 +- recipes/common.php | 12 +- recipes/simple.php | 2 +- src/Files.php | 13 +- src/Rsync.php | 134 +++++++++++++++------ tasks/database.php | 33 ++--- tasks/files.php | 33 +++-- tasks/languages.php | 33 ++--- tasks/mu-plugins.php | 41 ++++--- tasks/packages.php | 57 ++++----- tasks/plugins.php | 33 ++--- tasks/themes.php | 55 ++++----- tasks/uploads.php | 33 ++--- tasks/wp.php | 32 ++--- tests/Integration/IntegrationTestCase.php | 8 +- tests/Integration/RsyncIntegrationTest.php | 31 +++-- tests/Unit/RsyncTest.php | 26 ++-- tests/Unit/UnitTestCase.php | 3 +- tests/bootstrap.php | 2 +- 24 files changed, 346 insertions(+), 267 deletions(-) diff --git a/composer.json b/composer.json index 53251d3..9510ed7 100644 --- a/composer.json +++ b/composer.json @@ -32,11 +32,11 @@ } }, "scripts": { - "phpcs": "phpcs", - "phpcs:fix": "phpcbf", - "phpstan": "phpstan analyse", + "phpcs": "vendor/bin/phpcs -s", + "phpcs:fix": "vendor/bin/phpcbf", + "phpstan": "vendor/bin/phpstan analyse --memory-limit 1G", "lint": "find . -type f -name '*.php' -not -path './vendor/*' -exec php -l {} \\;", - "test": "phpunit", + "test": "vendor/bin/phpunit", "precommit": [ "@lint", "@phpcs", diff --git a/examples/bedrock/deploy.php b/examples/bedrock/deploy.php index 36e7bd1..320572b 100644 --- a/examples/bedrock/deploy.php +++ b/examples/bedrock/deploy.php @@ -16,7 +16,7 @@ // hosts & config import('deploy.yml'); -// OPTIONAL: overwrite localhost config' +// OPTIONAL: overwrite localhost config. localhost() ->set('public_url', "{{local_url}}") ->set('deploy_path', __DIR__) diff --git a/examples/simple/deploy.php b/examples/simple/deploy.php index 4fce2bf..674a32c 100644 --- a/examples/simple/deploy.php +++ b/examples/simple/deploy.php @@ -16,7 +16,7 @@ // hosts & config import('deploy.yml'); -// OPTIONAL: overwrite localhost config' +// OPTIONAL: overwrite localhost config. localhost() ->set('public_url', "{{local_url}}") ->set('deploy_path', __DIR__) diff --git a/phpcs.xml b/phpcs.xml index 62b18fc..4a810dc 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -5,13 +5,17 @@ - src - recipes - examples + ./src + ./tasks + ./recipes + ./examples + + + vendor/* - \ No newline at end of file + diff --git a/phpstan.neon b/phpstan.neon index 44b8b61..de16051 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,9 +1,9 @@ parameters: - level: 5 + level: 6 paths: - src + - tasks - recipes - examples - - tasks scanDirectories: - - vendor/deployer/deployer \ No newline at end of file + - vendor/deployer/deployer diff --git a/recipes/bedrock.php b/recipes/bedrock.php index bdbe37b..5b535b0 100644 --- a/recipes/bedrock.php +++ b/recipes/bedrock.php @@ -1,7 +1,7 @@ '.deployfilter', 'flags' => 'rz', // Recursive, with compress - 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterwards + 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterward 'timeout' => 60, 'progress_bar' => true, ]; diff --git a/recipes/simple.php b/recipes/simple.php index 95c353c..cc4e7a4 100644 --- a/recipes/simple.php +++ b/recipes/simple.php @@ -1,7 +1,7 @@ , + * 'exclude-file'?: string|false, + * include?: array, + * 'include-file'?: string|false, + * filter?: array, + * 'filter-file'?: string|false, + * 'filter-perdir'?: string|false, + * options?: array + * } + * + * @phpstan-type RsyncOptions list + * + */ class Rsync { /** - * Build rsync options array + * Builds a comprehensive array of rsync command options + * by merging default (set in `rsync` config) and custom configurations. * Takes a full config array and builds the different command-line arguments to be passed to rsync - * Combines all config-build functions of this file - * - * @param array $config - * $config = [ - * 'exclude' => (array) Array of paths/files to exclude - * 'exclude-file' => (string) Path to a exclude file to pass to rsync - * 'include' => (array) Array of paths/files to include - * 'include-file' => (string) Path to a include file to pass to rsync - * 'filter' => (array) Array of rsync filters - * 'filter-file' => (string) Path to a filterFile in rsync filter syntax - * 'filter-perdir' => (string) Filename to be used on a per directory basis for rsync filtering - * 'options' => (array) Array of options/flags to be passed to rsync - do not include dashes! - * ] - * @return array + * Combines all config-build functions of this file. + * + * @example + * // Using default configuration + * $options = buildRsyncOptions(); + * // Returns ['--delete-after', '--exclude=.git', '--exclude=deploy.php'] + * + * @example + * // With custom configuration + * $options = buildRsyncOptions([ + * 'exclude' => ['.git', 'node_modules'], + * 'include' => ['dist'], + * 'options' => ['archive', 'verbose'] + * ]); + * // Returns ['--archive', '--verbose', '--include=dist', '--exclude=.git', '--exclude=node_modules'] + * + * @param RsyncConfig $config Custom rsync configuration. + * @return RsyncOptions Array of formatted rsync command options with empty values filtered out */ public static function buildOptionsArray(array $config = []): array { @@ -39,7 +59,7 @@ public static function buildOptionsArray(array $config = []): array 'filter' => [], 'filter-file' => false, 'filter-perdir' => false, - 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterwards + 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterward ]; } @@ -64,11 +84,22 @@ public static function buildOptionsArray(array $config = []): array } /** - * Build rsync options and flags - * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options + * Builds an array of rsync option arguments by prefixing each option with '--'. + * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options. * - * @param array $options Array of options/flags to be passed to rsync - do not include dashes! - * @return array Options as command-line arguments which can be passed to rsync + * @example + * // Returns ['--archive', '--verbose', '--compress'] + * buildOptions(['archive', 'verbose', 'compress']); + * + * @example + * // Returns ['--dry-run', '--itemize-changes'] + * buildOptions(['dry-run', 'itemize-changes']); + * + * @param list $options Array of rsync option names without the '--' prefix + * (e.g. ['archive', 'verbose', 'compress']) + * + * @return list Array of formatted option arguments where each element + * has the format '--{option_name}' */ public static function buildOptions(array $options): array { @@ -81,12 +112,22 @@ public static function buildOptions(array $options): array } /** - * Build includes command-line arguments from array/file + * Builds an array of rsync include arguments based on provided include patterns and include file. * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options + * @example + * // Returns ['--include=*.php', '--include=/config/'] + * buildIncludes(['*.php', '/config/']); + * + * @example + * // Returns ['--include=*.php', '--include-from=/path/to/include-list.txt'] + * buildIncludes(['*.php'], '/path/to/include-list.txt'); * - * @param array $includes Array of paths/files to include - * @param string|null $includeFile Path to a include file to pass to rsync - * @return array Includes as command-line arguments which can be passed to rsync + * @param list $includes Array of include patterns to be applied (e.g. ["*.php", "/config/"]) + * @param string|null $includeFile Path to a file containing include patterns (e.g. "/path/to/include-list.txt") + * + * @return list Array where each element is one of: + * - '--include={include_pattern}' for each entry in $includes + * - '--include-from={include_file_path}' if $includeFile is valid */ public static function buildIncludes(array $includes = [], ?string $includeFile = null): array { @@ -103,12 +144,23 @@ public static function buildIncludes(array $includes = [], ?string $includeFile } /** - * Build excludes command-line arguments from array/file + * Builds an array of rsync exclude arguments based on provided exclude patterns and exclude file. * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options * - * @param array $excludes Array of paths/files to exclude - * @param string|null $excludeFile Path to a exclude file to pass to rsync - * @return array Excludes as command-line arguments which can be passed to rsync + * @example + * // Returns ['--exclude=*.tmp', '--exclude=/cache/'] + * buildExcludes(['*.tmp', '/cache/']); + * + * @example + * // Returns ['--exclude=*.tmp', '--exclude-from=/path/to/exclude-list.txt'] + * buildExcludes(['*.tmp'], '/path/to/exclude-list.txt'); + * + * @param list $excludes Array of exclude patterns to be applied (e.g. ["*.tmp", "/cache/"]) + * @param string|null $excludeFile Path to a file containing exclude patterns (e.g. "/path/to/exclude-list.txt") + * + * @return list Array where each element is one of: + * - '--exclude={exclude_pattern}' for each entry in $excludes + * - '--exclude-from={exclude_file_path}' if $excludeFile is valid */ public static function buildExcludes(array $excludes = [], ?string $excludeFile = null): array { @@ -125,13 +177,25 @@ public static function buildExcludes(array $excludes = [], ?string $excludeFile } /** - * Build filter command-line arguments from array/file/filePerDir + * Builds an array of rsync filter arguments based on provided filters and filter files. * Does not use escapeshellarg, because Rsync class from deployer uses it and that would destroy the options * - * @param array $filters Array of rsync filters - * @param string|null $filterFile Path to a filterFile in rsync filter syntax - * @param string|null $filterPerDir Filename to be used on a per directory basis for rsync filtering - * @return array Filters as command-line arguments which can be passed to rsync + * @example + * // Returns ['--filter=exclude=/tmp', '--filter=include=/var'] + * buildFilter(['exclude=/tmp', 'include=/var']); + * + * @example + * // Returns ['--filter=exclude=/tmp', '--filter=merge /path/to/rules.txt', '--filter=dir-merge .rsync-filter'] + * buildFilter(['exclude=/tmp'], '/path/to/rules.txt', '.rsync-filter'); + * + * @param list $filters Array of filter patterns to be applied (e.g. ["exclude=/path", "include=/other"]) + * @param string|null $filterFile Path to a filter file to be merged (e.g. "/path/to/filter-rules.txt") + * @param string|null $filterPerDir Name of per-directory filter files to be merged (e.g. ".rsync-filter") + * + * @return list Array where each element is one of: + * - '--filter={filter_pattern}' for each entry in $filters + * - '--filter=merge {filter_file_path}' if $filterFile is valid + * - '--filter=dir-merge {filter_per_dir}' if $filterPerDir is provided */ public static function buildFilter( array $filters = [], @@ -143,10 +207,10 @@ public static function buildFilter( $filtersStrings[] = '--filter=' . $filter; } if (!empty($filterFile) && file_exists($filterFile) && is_file($filterFile) && is_readable($filterFile)) { - $filtersStrings[] = "--filter=merge " . $filterFile . ""; + $filtersStrings[] = "--filter=merge " . $filterFile; } if (!empty($filterPerDir)) { - $filtersStrings[] = "--filter=dir-merge " . $filterPerDir . ""; + $filtersStrings[] = "--filter=dir-merge " . $filterPerDir; } return $filtersStrings; } diff --git a/tasks/database.php b/tasks/database.php index 1906c1e..d10a060 100644 --- a/tasks/database.php +++ b/tasks/database.php @@ -13,6 +13,9 @@ namespace Gaambo\DeployerWordpress\Tasks; +use Gaambo\DeployerWordpress\Localhost; +use Gaambo\DeployerWordpress\WPCLI; + use function Deployer\download; use function Deployer\get; use function Deployer\run; @@ -20,16 +23,14 @@ use function Deployer\set; use function Deployer\task; use function Deployer\upload; -use Gaambo\DeployerWordpress\Localhost; -use Gaambo\DeployerWordpress\WPCLI; /** * Create backup of remote database and download locally - * + * * Configuration: * - dump_path: Directory to store database dumps (required on both local and remote) * - bin/wp: WP-CLI binary/command to use (automatically configured) - * + * * Example: * dep db:remote:backup prod */ @@ -48,11 +49,11 @@ /** * Create backup of local database and upload to remote - * + * * Configuration: * - dump_path: Directory to store database dumps (required on both local and remote) * - bin/wp: WP-CLI binary/command to use (automatically configured) - * + * * Example: * dep db:local:backup prod */ @@ -74,12 +75,12 @@ /** * Import database backup on remote host - * + * * Configuration: * - bin/wp: WP-CLI binary/command to use (automatically configured) * - public_url: Site URL for both local and remote (required for URL replacement) * - uploads/dir: Upload directory path (for path replacement if different between environments) - * + * * Example: * dep db:remote:import prod */ @@ -88,7 +89,7 @@ WPCLI::runCommand("db import {{dump_filepath}}", "{{release_or_current_path}}"); WPCLI::runCommand("search-replace $localUrl {{public_url}}", "{{release_or_current_path}}"); - // If the local uploads directory is different than the remote one + // If the local uploads directory is different from the remote one // replace all references to the local uploads directory with the remote one $localUploadsDir = Localhost::getConfig('uploads/dir'); if ($localUploadsDir !== get('uploads/dir')) { @@ -100,13 +101,13 @@ /** * Import database backup on local host - * + * * Configuration: * - bin/wp: WP-CLI binary/command to use (automatically configured) * - public_url: Site URL for both local and remote (required for URL replacement) * - uploads/dir: Upload directory path (for path replacement if different between environments) * - dump_path: Directory containing database dumps - * + * * Example: * dep db:local:import prod */ @@ -116,7 +117,7 @@ WPCLI::runCommandLocally("db import $localDumpPath/{{dump_file}}"); WPCLI::runCommandLocally("search-replace {{public_url}} $localUrl"); - // If the local uploads directory is different than the remote one + // If the local uploads directory is different from the remote one // replace all references to the remotes uploads directory with the local one $localUploadsDir = Localhost::getConfig('uploads/dir'); if ($localUploadsDir !== get('uploads/dir')) { @@ -128,10 +129,10 @@ /** * Push database from local to remote - * + * * Combines db:local:backup and db:remote:import tasks. * See individual tasks for configuration options. - * + * * Example: * dep db:push prod */ @@ -140,10 +141,10 @@ /** * Pull database from remote to local - * + * * Combines db:remote:backup and db:local:import tasks. * See individual tasks for configuration options. - * + * * Example: * dep db:pull prod */ diff --git a/tasks/files.php b/tasks/files.php index 9dd4341..e6c607a 100644 --- a/tasks/files.php +++ b/tasks/files.php @@ -13,12 +13,11 @@ namespace Gaambo\DeployerWordpress\Tasks; -use function Deployer\download; -use function Deployer\get; -use function Deployer\task; use Gaambo\DeployerWordpress\Files; use Gaambo\DeployerWordpress\Localhost; -use Gaambo\DeployerWordpress\Rsync; + +use function Deployer\download; +use function Deployer\task; require_once __DIR__ . '/mu-plugins.php'; require_once __DIR__ . '/packages.php'; @@ -29,10 +28,10 @@ /** * Push all files from local to remote - * + * * Runs wp:push, uploads:push, plugins:push, mu-plugins:push, themes:push, packages:push in series. * See individual task definitions for required configuration options. - * + * * Example: * dep files:push prod */ @@ -41,10 +40,10 @@ /** * Pull all files from remote to local - * + * * Runs wp:pull, uploads:pull, plugins:pull, mu-plugins:pull, themes:pull, packages:pull in series. * See individual task definitions for required configuration options. - * + * * Example: * dep files:pull prod */ @@ -53,10 +52,10 @@ /** * Sync all files between remote and local - * + * * Combines files:push and files:pull tasks. * See individual tasks for configuration options. - * + * * Example: * dep files:sync prod */ @@ -65,13 +64,13 @@ /** * Backup all files on remote host - * + * * Creates a zip backup of remote WordPress files and downloads it locally. - * + * * Configuration: * - backup_path: Path for storing backups (required on both local and remote) * - release_path: Path to WordPress installation on remote - * + * * Example: * dep files:backup:remote prod */ @@ -87,20 +86,20 @@ /** * Backup all files on local host - * + * * Creates a zip backup of local WordPress files. - * + * * Configuration: * - backup_path: Path for storing backups (local) * - current_path: Path to WordPress installation on local - * + * * Example: * dep files:backup:local prod */ task('files:backup:local', function () { $localPath = Localhost::getConfig('current_path'); $localBackupPath = Localhost::getConfig('backup_path'); - $backupFile = Files::zipFiles( + Files::zipFiles( "$localPath/", $localBackupPath, 'backup_files' diff --git a/tasks/languages.php b/tasks/languages.php index b5609fe..c4753ca 100644 --- a/tasks/languages.php +++ b/tasks/languages.php @@ -12,20 +12,21 @@ namespace Gaambo\DeployerWordpress\Tasks; -use function Deployer\download; -use function Deployer\get; -use function Deployer\task; use Gaambo\DeployerWordpress\Files; use Gaambo\DeployerWordpress\Localhost; use Gaambo\DeployerWordpress\Rsync; +use function Deployer\download; +use function Deployer\get; +use function Deployer\task; + /** * Push languages from local to remote - * + * * Configuration: * - languages/dir: Path to languages directory relative to document root * - languages/filter: Rsync filter rules for language files (has defaults) - * + * * Example: * dep languages:push prod */ @@ -38,11 +39,11 @@ /** * Pull languages from remote to local - * + * * Configuration: * - languages/dir: Path to languages directory relative to document root * - languages/filter: Rsync filter rules for language files (has defaults) - * + * * Example: * dep languages:pull prod */ @@ -55,10 +56,10 @@ /** * Sync languages between remote and local - * + * * Combines languages:push and languages:pull tasks. * See individual tasks for configuration options. - * + * * Example: * dep languages:sync prod */ @@ -67,13 +68,13 @@ /** * Backup languages on remote host - * + * * Creates a zip backup of remote languages and downloads it locally. - * + * * Configuration: * - languages/dir: Path to languages directory relative to document root * - backup_path: Path for storing backups (required on both local and remote) - * + * * Example: * dep languages:backup:remote prod */ @@ -89,20 +90,20 @@ /** * Backup languages on local host - * + * * Creates a zip backup of local languages. - * + * * Configuration: * - languages/dir: Path to languages directory relative to document root * - backup_path: Path for storing backups (local) - * + * * Example: * dep languages:backup:local prod */ task('languages:backup:local', function () { $localPath = Localhost::getConfig('current_path'); $localBackupPath = Localhost::getConfig('backup_path'); - $backupFile = Files::zipFiles( + Files::zipFiles( "$localPath/{{languages/dir}}/", $localBackupPath, 'backup_languages' diff --git a/tasks/mu-plugins.php b/tasks/mu-plugins.php index 05c246a..3686006 100644 --- a/tasks/mu-plugins.php +++ b/tasks/mu-plugins.php @@ -13,22 +13,23 @@ namespace Gaambo\DeployerWordpress\Tasks; -use function Deployer\download; -use function Deployer\get; -use function Deployer\task; use Gaambo\DeployerWordpress\Composer; use Gaambo\DeployerWordpress\Files; use Gaambo\DeployerWordpress\Localhost; use Gaambo\DeployerWordpress\Rsync; +use function Deployer\download; +use function Deployer\get; +use function Deployer\task; + /** * Install mu-plugin vendors via composer - * + * * Configuration: * - mu-plugins/dir: Path to mu-plugins directory relative to document root * - mu-plugin/name: Name (directory) of your custom mu-plugin * - bin/composer: Composer binary/command to use (automatically configured) - * + * * Example: * dep mu-plugin:vendors prod */ @@ -38,10 +39,10 @@ /** * Install all mu-plugin dependencies - * + * * Currently only runs mu-plugin:vendors task. * See individual tasks for configuration options. - * + * * Example: * dep mu-plugin prod */ @@ -50,11 +51,11 @@ /** * Push mu-plugins from local to remote - * + * * Configuration: * - mu-plugins/dir: Path to mu-plugins directory relative to document root * - mu-plugins/filter: Rsync filter rules for mu-plugin files (has defaults) - * + * * Example: * dep mu-plugins:push prod */ @@ -67,11 +68,11 @@ /** * Pull mu-plugins from remote to local - * + * * Configuration: * - mu-plugins/dir: Path to mu-plugins directory relative to document root * - mu-plugins/filter: Rsync filter rules for mu-plugin files (has defaults) - * + * * Example: * dep mu-plugins:pull prod */ @@ -84,10 +85,10 @@ /** * Sync mu-plugins between remote and local - * + * * Combines mu-plugins:push and mu-plugins:pull tasks. * See individual tasks for configuration options. - * + * * Example: * dep mu-plugins:sync prod */ @@ -96,13 +97,13 @@ /** * Backup mu-plugins on remote host - * + * * Creates a zip backup of remote mu-plugins and downloads it locally. - * + * * Configuration: * - mu-plugins/dir: Path to mu-plugins directory relative to document root * - backup_path: Path for storing backups (required on both local and remote) - * + * * Example: * dep mu-plugins:backup:remote prod */ @@ -118,20 +119,20 @@ /** * Backup mu-plugins on local host - * + * * Creates a zip backup of local mu-plugins. - * + * * Configuration: * - mu-plugins/dir: Path to mu-plugins directory relative to document root * - backup_path: Path for storing backups (local) - * + * * Example: * dep mu-plugins:backup:local prod */ task('mu-plugins:backup:local', function () { $localPath = Localhost::getConfig('current_path'); $localBackupPath = Localhost::getConfig('backup_path'); - $backupFile = Files::zipFiles( + Files::zipFiles( "$localPath/{{mu-plugins/dir}}/", $localBackupPath, 'backup_mu-plugins' diff --git a/tasks/packages.php b/tasks/packages.php index 2908d4b..3d89f7d 100644 --- a/tasks/packages.php +++ b/tasks/packages.php @@ -7,7 +7,7 @@ * - Pushing/pulling package files between environments * - Creating backups of package directories * - Managing package dependencies and configurations - * + * * Packages can be custom themes, plugins, or mu-plugins that need special handling. * * @package Gaambo\DeployerWordpress\Tasks @@ -15,23 +15,24 @@ namespace Gaambo\DeployerWordpress\Tasks; -use function Deployer\download; -use function Deployer\get; -use function Deployer\run; -use function Deployer\task; use Gaambo\DeployerWordpress\Composer; use Gaambo\DeployerWordpress\Files; use Gaambo\DeployerWordpress\Localhost; use Gaambo\DeployerWordpress\NPM; use Gaambo\DeployerWordpress\Rsync; +use function Deployer\download; +use function Deployer\get; +use function Deployer\run; +use function Deployer\task; + /** * Install package assets dependencies via npm - * + * * Configuration per package: * - path: Path of package relative to release_path/current_path * - assets: Whether the package has assets to install - * + * * Example: * dep packages:assets:vendors prod */ @@ -47,12 +48,12 @@ /** * Build package assets via npm script - * + * * Configuration per package: * - path: Path of package relative to release_path/current_path * - assets: Whether the package has assets to build * - assets:build_script: NPM script to run (optional, default: "build") - * + * * Example: * dep packages:assets:build prod */ @@ -71,10 +72,10 @@ /** * Install and build package assets - * + * * Combines packages:assets:vendors and packages:assets:build tasks. * See individual tasks for configuration options. - * + * * Example: * dep packages:assets prod */ @@ -83,10 +84,10 @@ /** * Install package dependencies via composer - * + * * Configuration per package: * - path: Path of package relative to release_path/current_path - * + * * Example: * dep packages:vendors prod */ @@ -101,10 +102,10 @@ /** * Install all package dependencies - * + * * Combines packages:assets and packages:vendors tasks. * See individual tasks for configuration options. - * + * * Example: * dep packages prod */ @@ -113,12 +114,12 @@ /** * Push packages from local to remote - * + * * Configuration per package: * - path: Path of package relative to release_path/current_path * - remote:path: Path of package on remote host * - rsync:filter: (optional) Rsync filter rules for package files - * + * * Example: * dep packages:push prod */ @@ -136,12 +137,12 @@ /** * Pull packages from remote to local - * + * * Configuration per package: * - path: Path of package relative to release_path/current_path * - remote:path: Path of package on remote host * - rsync:filter: (optional) Rsync filter rules for package files - * + * * Example: * dep packages:pull prod */ @@ -158,10 +159,10 @@ /** * Sync packages between remote and local - * + * * Combines packages:push and packages:pull tasks. * See individual tasks for configuration options. - * + * * Example: * dep packages:sync prod */ @@ -170,13 +171,13 @@ /** * Backup packages on remote host - * + * * Creates a zip backup of remote packages and downloads it locally. - * + * * Configuration per package: * - remote:path: Path of package on remote host * - backup_path: Path for storing backups (required on both local and remote) - * + * * Example: * dep packages:backup:remote prod */ @@ -195,13 +196,13 @@ /** * Backup packages on local host - * + * * Creates a zip backup of local packages. - * + * * Configuration per package: * - path: Path of package relative to release_path/current_path * - backup_path: Path for storing backups (local) - * + * * Example: * dep packages:backup:local prod */ @@ -211,7 +212,7 @@ foreach (get('packages', []) as $package) { $packagePath = $package['path']; - $backupFile = Files::zipFiles( + Files::zipFiles( "$localPath/$packagePath/", $localBackupPath, 'backup_packages' diff --git a/tasks/plugins.php b/tasks/plugins.php index 8444b29..6c0931f 100644 --- a/tasks/plugins.php +++ b/tasks/plugins.php @@ -12,20 +12,21 @@ namespace Gaambo\DeployerWordpress\Tasks; -use function Deployer\download; -use function Deployer\get; -use function Deployer\task; use Gaambo\DeployerWordpress\Files; use Gaambo\DeployerWordpress\Localhost; use Gaambo\DeployerWordpress\Rsync; +use function Deployer\download; +use function Deployer\get; +use function Deployer\task; + /** * Push plugins from local to remote - * + * * Configuration: * - plugins/dir: Path to plugins directory relative to document root * - plugins/filter: Rsync filter rules for plugin files (has defaults) - * + * * Example: * dep plugins:push prod */ @@ -38,11 +39,11 @@ /** * Pull plugins from remote to local - * + * * Configuration: * - plugins/dir: Path to plugins directory relative to document root * - plugins/filter: Rsync filter rules for plugin files (has defaults) - * + * * Example: * dep plugins:pull prod */ @@ -55,10 +56,10 @@ /** * Sync plugins between remote and local - * + * * Combines plugins:push and plugins:pull tasks. * See individual tasks for configuration options. - * + * * Example: * dep plugins:sync prod */ @@ -67,13 +68,13 @@ /** * Backup plugins on remote host - * + * * Creates a zip backup of remote plugins and downloads it locally. - * + * * Configuration: * - plugins/dir: Path to plugins directory relative to document root * - backup_path: Path for storing backups (required on both local and remote) - * + * * Example: * dep plugins:backup:remote prod */ @@ -89,20 +90,20 @@ /** * Backup plugins on local host - * + * * Creates a zip backup of local plugins. - * + * * Configuration: * - plugins/dir: Path to plugins directory relative to document root * - backup_path: Path for storing backups (local) - * + * * Example: * dep plugins:backup:local prod */ task('plugins:backup:local', function () { $localPath = Localhost::getConfig('current_path'); $localBackupPath = Localhost::getConfig('backup_path'); - $backupFile = Files::zipFiles( + Files::zipFiles( "$localPath/{{plugins/dir}}/", $localBackupPath, 'backup_plugins' diff --git a/tasks/themes.php b/tasks/themes.php index 45b257b..885a465 100644 --- a/tasks/themes.php +++ b/tasks/themes.php @@ -14,18 +14,19 @@ namespace Gaambo\DeployerWordpress\Tasks; -use function Deployer\download; -use function Deployer\get; -use function Deployer\task; use Gaambo\DeployerWordpress\Composer; use Gaambo\DeployerWordpress\Files; use Gaambo\DeployerWordpress\Localhost; use Gaambo\DeployerWordpress\NPM; use Gaambo\DeployerWordpress\Rsync; +use function Deployer\download; +use function Deployer\get; +use function Deployer\task; + /** * Theme Tasks - * + * * Provides tasks for managing WordPress themes: * - Installing theme dependencies (npm, composer) * - Building theme assets @@ -35,12 +36,12 @@ /** * Install theme assets dependencies via npm - * + * * Configuration: * - themes/dir: Path to themes directory relative to document root * - theme/name: Name (directory) of your custom theme * - bin/npm: NPM binary/command to use (automatically configured) - * + * * Example: * dep theme:assets:vendors prod */ @@ -50,13 +51,13 @@ /** * Build theme assets via npm script - * + * * Configuration: * - themes/dir: Path to themes directory relative to document root * - theme/name: Name (directory) of your custom theme * - theme/build_script: NPM script to run (default: 'build') * - bin/npm: NPM binary/command to use (automatically configured) - * + * * Example: * dep theme:assets:build prod * dep theme:assets:build prod -o theme/build_script=dev @@ -70,10 +71,10 @@ /** * Install theme assets and run build script - * + * * Combines theme:assets:vendors and theme:assets:build tasks. * See individual tasks for configuration options. - * + * * Example: * dep theme:assets prod */ @@ -82,12 +83,12 @@ /** * Install theme vendors via composer - * + * * Configuration: * - themes/dir: Path to themes directory relative to document root * - theme/name: Name (directory) of your custom theme * - bin/composer: Composer binary/command to use (automatically configured) - * + * * Example: * dep theme:vendors prod */ @@ -97,10 +98,10 @@ /** * Install all theme dependencies and build assets - * + * * Combines theme:assets and theme:vendors tasks. * See individual tasks for configuration options. - * + * * Example: * dep theme prod */ @@ -109,11 +110,11 @@ /** * Push themes from local to remote - * + * * Configuration: * - themes/dir: Path to themes directory relative to document root * - themes/filter: Rsync filter rules for theme files (has defaults) - * + * * Example: * dep themes:push prod */ @@ -126,11 +127,11 @@ /** * Pull themes from remote to local - * + * * Configuration: * - themes/dir: Path to themes directory relative to document root * - themes/filter: Rsync filter rules for theme files (has defaults) - * + * * Example: * dep themes:pull prod */ @@ -143,10 +144,10 @@ /** * Sync themes between remote and local - * + * * Combines themes:push and themes:pull tasks. * See individual tasks for configuration options. - * + * * Example: * dep themes:sync prod */ @@ -155,13 +156,13 @@ /** * Backup themes on remote host - * + * * Creates a zip backup of remote themes and downloads it locally. - * + * * Configuration: * - themes/dir: Path to themes directory relative to document root * - backup_path: Path for storing backups (required on both local and remote) - * + * * Example: * dep themes:backup:remote prod */ @@ -177,20 +178,20 @@ /** * Backup themes on local host - * + * * Creates a zip backup of local themes. - * + * * Configuration: * - themes/dir: Path to themes directory relative to document root * - backup_path: Path for storing backups (local) - * + * * Example: * dep themes:backup:local prod */ task('themes:backup:local', function () { $localPath = Localhost::getConfig('current_path'); $localBackupPath = Localhost::getConfig('backup_path'); - $backupFile = Files::zipFiles( + Files::zipFiles( "$localPath/{{themes/dir}}/", $localBackupPath, 'backup_themes' diff --git a/tasks/uploads.php b/tasks/uploads.php index 71bb0b4..c102e43 100644 --- a/tasks/uploads.php +++ b/tasks/uploads.php @@ -12,22 +12,23 @@ namespace Gaambo\DeployerWordpress\Tasks; +use Gaambo\DeployerWordpress\Files; +use Gaambo\DeployerWordpress\Localhost; +use Gaambo\DeployerWordpress\Rsync; + use function Deployer\download; use function Deployer\get; use function Deployer\task; use function Deployer\upload; -use Gaambo\DeployerWordpress\Files; -use Gaambo\DeployerWordpress\Localhost; -use Gaambo\DeployerWordpress\Rsync; /** * Push uploads from local to remote - * + * * Configuration: * - uploads/dir: Path to uploads directory relative to document root * - uploads/path: Path to directory containing uploads (e.g., shared directory) * - uploads/filter: Rsync filter rules for upload files (has defaults) - * + * * Example: * dep uploads:push prod */ @@ -47,12 +48,12 @@ /** * Pull uploads from remote to local - * + * * Configuration: * - uploads/dir: Path to uploads directory relative to document root * - uploads/path: Path to directory containing uploads (e.g., shared directory) * - uploads/filter: Rsync filter rules for upload files (has defaults) - * + * * Example: * dep uploads:pull prod */ @@ -67,10 +68,10 @@ /** * Sync uploads between remote and local - * + * * Combines uploads:push and uploads:pull tasks. * See individual tasks for configuration options. - * + * * Example: * dep uploads:sync prod */ @@ -79,14 +80,14 @@ /** * Backup uploads on remote host - * + * * Creates a zip backup of remote uploads and downloads it locally. - * + * * Configuration: * - uploads/dir: Path to uploads directory relative to document root * - uploads/path: Path to directory containing uploads (e.g., shared directory) * - backup_path: Path for storing backups (required on both local and remote) - * + * * Example: * dep uploads:backup:remote prod */ @@ -102,14 +103,14 @@ /** * Backup uploads on local host - * + * * Creates a zip backup of local uploads. - * + * * Configuration: * - uploads/dir: Path to uploads directory relative to document root * - uploads/path: Path to directory containing uploads (e.g., shared directory) * - backup_path: Path for storing backups (local) - * + * * Example: * dep uploads:backup:local prod */ @@ -117,7 +118,7 @@ $localUploadsPath = Localhost::getConfig('uploads/path'); $localUploadsDir = Localhost::getConfig('uploads/dir'); $localBackupPath = Localhost::getConfig('backup_path'); - $backupFile = Files::zipFiles( + Files::zipFiles( "$localUploadsPath/$localUploadsDir/", $localBackupPath, 'backup_uploads' diff --git a/tasks/wp.php b/tasks/wp.php index a4e3d00..4968a87 100644 --- a/tasks/wp.php +++ b/tasks/wp.php @@ -14,20 +14,22 @@ namespace Gaambo\DeployerWordpress\Tasks; -use function Deployer\get; -use function Deployer\task; +use RuntimeException; use Gaambo\DeployerWordpress\Files; use Gaambo\DeployerWordpress\Localhost; use Gaambo\DeployerWordpress\Rsync; use Gaambo\DeployerWordpress\WPCLI; +use function Deployer\get; +use function Deployer\task; + /** * Download WordPress core files - * + * * Configuration: * - wp/version: WordPress version to download (optional, defaults to latest) * - bin/wp: WP-CLI binary/command to use (automatically configured) - * + * * Example: * dep wp:download-core production * dep wp:download-core production -o wp/version=6.4.3 @@ -38,11 +40,11 @@ /** * Push WordPress core files from local to remote - * + * * Configuration: * - wp/dir: Path to WordPress directory relative to document root * - wp/filter: Rsync filter rules for WordPress core files (has defaults) - * + * * Example: * dep wp:push prod */ @@ -56,11 +58,11 @@ /** * Pull WordPress core files from remote to local - * + * * Configuration: * - wp/dir: Path to WordPress directory relative to document root * - wp/filter: Rsync filter rules for WordPress core files (has defaults) - * + * * Example: * dep wp:pull prod */ @@ -69,18 +71,18 @@ $rsyncOptions = Rsync::buildOptionsArray([ 'filter' => get("wp/filter"), ]); - Files::pullFiles( '{{wp/dir}}', $localWpDir, $rsyncOptions); + Files::pullFiles('{{wp/dir}}', $localWpDir, $rsyncOptions); })->desc('Pull WordPress core files from remote to local'); /** * Display WP-CLI information - * + * * Useful for debugging WP-CLI setup and configuration. * Shows version, PHP info, and config paths. - * + * * Configuration: * - bin/wp: WP-CLI binary/command to use (automatically configured) - * + * * Example: * dep wp:info prod */ @@ -90,12 +92,12 @@ /** * Install WP-CLI on remote host - * + * * Configuration (via CLI options): * - installPath: Directory to install WP-CLI in (required) * - binaryFile: Name of the WP-CLI binary (default: wp-cli.phar) * - sudo: Whether to use sudo for installation (default: false) - * + * * Example: * dep wp:install-wpcli prod -o installPath=/usr/local/bin -o binaryFile=wp * dep wp:install-wpcli prod -o installPath=/usr/local/bin -o sudo=true @@ -103,7 +105,7 @@ task('wp:install-wpcli', function () { $installPath = get('installPath'); if (empty($installPath)) { - throw new \RuntimeException( + throw new RuntimeException( 'You have to set an installPath for WordPress via a config variable or in cli via `-o installPath=$path`.' ); } diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index be22569..bbddd23 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -36,7 +36,7 @@ protected function setUp(): void // Create a localhost instance for testing $this->host = new Localhost(); - + // Push a new context with our host Context::push(new Context($this->host)); } @@ -45,10 +45,10 @@ protected function tearDown(): void { // Pop the context to clean up Context::pop(); - + // Clean up the Deployer instance unset($this->deployer); - + parent::tearDown(); } -} \ No newline at end of file +} diff --git a/tests/Integration/RsyncIntegrationTest.php b/tests/Integration/RsyncIntegrationTest.php index 702cee8..61da3b1 100644 --- a/tests/Integration/RsyncIntegrationTest.php +++ b/tests/Integration/RsyncIntegrationTest.php @@ -3,7 +3,6 @@ namespace Gaambo\DeployerWordpress\Tests\Integration; use Gaambo\DeployerWordpress\Rsync; -use PHPUnit\Framework\MockObject\MockObject; class RsyncIntegrationTest extends IntegrationTestCase { @@ -70,9 +69,9 @@ public function testBuildOptionsArrayWithCustomConfig(): void 'filter-perdir' => '.deployfilter', 'options' => ['delete-after', 'recursive'] ]; - + $result = Rsync::buildOptionsArray($config); - + $this->assertContains('--exclude=*.log', $result); $this->assertContains('--exclude=*.tmp', $result); $this->assertContains('--exclude-from=' . $config['exclude-file'], $result); @@ -89,12 +88,12 @@ public function testBuildOptionsArrayWithCustomConfig(): void public function testBuildOptionsArrayWithDefaults(): void { $result = Rsync::buildOptionsArray(); - + // Default config should include these $this->assertContains('--exclude=.git', $result); $this->assertContains('--exclude=deploy.php', $result); $this->assertContains('--delete-after', $result); - + // These should not be present in default config $this->assertNotContains('--include=', $result); $this->assertNotContains('--include-from=', $result); @@ -106,7 +105,7 @@ public function testBuildOptionsArrayWithDefaults(): void public function testBuildOptionsArrayWithEmptyConfig(): void { $result = Rsync::buildOptionsArray([]); - + // Should still include defaults $this->assertContains('--exclude=.git', $result); $this->assertContains('--exclude=deploy.php', $result); @@ -117,9 +116,9 @@ public function testBuildOptionsArrayWithNoDeployerConfig(): void { // Remove the rsync config $this->deployer->config->set('rsync', null); - + $result = Rsync::buildOptionsArray(); - + // Should use hardcoded defaults $this->assertContains('--exclude=.git', $result); $this->assertContains('--exclude=deploy.php', $result); @@ -130,9 +129,9 @@ public function testBuildOptionsArrayWithInvalidDeployerConfig(): void { // Set an invalid config $this->deployer->config->set('rsync', 'invalid'); - + $result = Rsync::buildOptionsArray(); - + // Should use hardcoded defaults $this->assertContains('--exclude=.git', $result); $this->assertContains('--exclude=deploy.php', $result); @@ -146,9 +145,9 @@ public function testBuildOptionsArrayWithInvalidFilePaths(): void 'include-file' => '/nonexistent/include.txt', 'filter-file' => '/nonexistent/filter.txt' ]; - + $result = Rsync::buildOptionsArray($config); - + // Invalid files should be ignored $this->assertNotContains('--exclude-from=/nonexistent/exclude.txt', $result); $this->assertNotContains('--include-from=/nonexistent/include.txt', $result); @@ -163,20 +162,20 @@ public function testBuildOptionsArrayFiltersEmptyStrings(): void 'include' => ['', '*.php', ''], 'filter' => ['', '+ /wp-content/', ''] ]; - + $result = Rsync::buildOptionsArray($config); - + // Check that non-empty values are present $this->assertContains('--delete-after', $result); $this->assertContains('--recursive', $result); $this->assertContains('--exclude=.git', $result); $this->assertContains('--include=*.php', $result); $this->assertContains('--filter=+ /wp-content/', $result); - + // Check that no empty values are present $this->assertNotContains('', $result, "Empty string found in result"); $this->assertNotContains('--exclude=', $result, "Empty exclude found in result"); $this->assertNotContains('--include=', $result, "Empty include found in result"); $this->assertNotContains('--filter=', $result, "Empty filter found in result"); } -} \ No newline at end of file +} diff --git a/tests/Unit/RsyncTest.php b/tests/Unit/RsyncTest.php index ad559ee..73a9b56 100644 --- a/tests/Unit/RsyncTest.php +++ b/tests/Unit/RsyncTest.php @@ -19,9 +19,9 @@ public function testBuildExcludesWithExistingFile() { $excludes = ['*.log', '*.tmp', '.DS_Store']; $excludeFile = $this->fixturesDir . '/exclude.txt'; - + $result = Rsync::buildExcludes($excludes, $excludeFile); - + $this->assertContains('--exclude=*.log', $result); $this->assertContains('--exclude=*.tmp', $result); $this->assertContains('--exclude=.DS_Store', $result); @@ -32,9 +32,9 @@ public function testBuildExcludesWithNonExistingFile() { $excludes = ['*.log', '*.tmp', '.DS_Store']; $excludeFile = '/path/to/nonexistent.txt'; - + $result = Rsync::buildExcludes($excludes, $excludeFile); - + $this->assertContains('--exclude=*.log', $result); $this->assertContains('--exclude=*.tmp', $result); $this->assertContains('--exclude=.DS_Store', $result); @@ -45,9 +45,9 @@ public function testBuildIncludesWithExistingFile() { $includes = ['*.php', '*.js', '*.css']; $includeFile = $this->fixturesDir . '/include.txt'; - + $result = Rsync::buildIncludes($includes, $includeFile); - + $this->assertContains('--include=*.php', $result); $this->assertContains('--include=*.js', $result); $this->assertContains('--include=*.css', $result); @@ -58,9 +58,9 @@ public function testBuildIncludesWithNonExistingFile() { $includes = ['*.php', '*.js', '*.css']; $includeFile = '/path/to/nonexistent.txt'; - + $result = Rsync::buildIncludes($includes, $includeFile); - + $this->assertContains('--include=*.php', $result); $this->assertContains('--include=*.js', $result); $this->assertContains('--include=*.css', $result); @@ -72,9 +72,9 @@ public function testBuildFilterWithExistingFile() $filters = ['+ /wp-content/', '- /wp-content/uploads/*']; $filterFile = $this->fixturesDir . '/filter.txt'; $filterPerDir = '.deployfilter'; - + $result = Rsync::buildFilter($filters, $filterFile, $filterPerDir); - + $this->assertContains('--filter=+ /wp-content/', $result); $this->assertContains('--filter=- /wp-content/uploads/*', $result); $this->assertContains('--filter=merge ' . $filterFile, $result); @@ -86,9 +86,9 @@ public function testBuildFilterWithNonExistingFile() $filters = ['+ /wp-content/', '- /wp-content/uploads/*']; $filterFile = '/path/to/nonexistent.txt'; $filterPerDir = '.deployfilter'; - + $result = Rsync::buildFilter($filters, $filterFile, $filterPerDir); - + $this->assertContains('--filter=+ /wp-content/', $result); $this->assertContains('--filter=- /wp-content/uploads/*', $result); $this->assertNotContains('--filter=merge ' . $filterFile, $result); @@ -147,4 +147,4 @@ public function testBuildFilterWithEmptyArray(): void $result = Rsync::buildFilter([]); $this->assertEmpty($result); } -} \ No newline at end of file +} diff --git a/tests/Unit/UnitTestCase.php b/tests/Unit/UnitTestCase.php index 6a7e4b3..49faa1e 100644 --- a/tests/Unit/UnitTestCase.php +++ b/tests/Unit/UnitTestCase.php @@ -9,6 +9,5 @@ abstract class UnitTestCase extends BaseTestCase protected function setUp(): void { parent::setUp(); - } -} \ No newline at end of file +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1f24445..c903491 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -19,4 +19,4 @@ // Create temp directory if it doesn't exist if (!file_exists(__TEMP_DIR__)) { mkdir(__TEMP_DIR__, 0755, true); -} \ No newline at end of file +} From 89b453e413d93806ac84623e94e169e9583dec83 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 12:20:50 +0100 Subject: [PATCH 13/36] add test workflow for github ci --- .github/workflows/{pull-request.yml => tests.yml} | 14 ++++++++++++++ 1 file changed, 14 insertions(+) rename .github/workflows/{pull-request.yml => tests.yml} (62%) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/tests.yml similarity index 62% rename from .github/workflows/pull-request.yml rename to .github/workflows/tests.yml index 539b2da..3ccd5ef 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/tests.yml @@ -2,6 +2,8 @@ name: Pull Request Tests on: pull_request: + # Allow manually triggering the workflow. + workflow_dispatch: jobs: tests: @@ -11,6 +13,10 @@ jobs: strategy: matrix: php: ['8.1', '8.2', '8.3', '8.4'] + # TODO: Add 8.0 when released. + deployer: ['^7.3', '^7.4', '^7.5'] + include: + - name: Deployer ${{ matrix.deployer }} on PHP ${{ matrix.php }} fail-fast: false steps: @@ -31,6 +37,14 @@ jobs: with: composer-options: '--prefer-dist --no-progress' + # Install specific Deployer version + - name: Install Deployer version + run: composer require --no-update deployer/deployer:"${{ matrix.deployer }}" + + # Run PHPStan + - name: Run PHPStan + run: composer run-script phpstan + # Run PHPUnit tests - name: Run PHPUnit tests run: composer run-script test \ No newline at end of file From 7975e2b3a7a72fb01cc7fc3040762dcd2185232a Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 12:25:24 +0100 Subject: [PATCH 14/36] try to fix tests matrix --- .github/workflows/tests.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3ccd5ef..7b97b74 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,6 @@ on: jobs: tests: - name: Run PHPUnit Tests runs-on: ubuntu-latest timeout-minutes: 10 strategy: @@ -15,9 +14,8 @@ jobs: php: ['8.1', '8.2', '8.3', '8.4'] # TODO: Add 8.0 when released. deployer: ['^7.3', '^7.4', '^7.5'] - include: - - name: Deployer ${{ matrix.deployer }} on PHP ${{ matrix.php }} fail-fast: false + name: Tests forDeployer ${{ matrix.deployer }} on PHP ${{ matrix.php }} steps: # Checkout the repository From 0939c359740a19231e887d70532f123b9618199c Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 12:31:20 +0100 Subject: [PATCH 15/36] try to fix tests in github actions --- .github/workflows/tests.yml | 9 +++++---- tests/bootstrap.php | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7b97b74..fe43552 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Pull Request Tests +name: Tests on: pull_request: @@ -13,9 +13,9 @@ jobs: matrix: php: ['8.1', '8.2', '8.3', '8.4'] # TODO: Add 8.0 when released. - deployer: ['^7.3', '^7.4', '^7.5'] + deployer: ['7.3', '7.4', '7.5'] fail-fast: false - name: Tests forDeployer ${{ matrix.deployer }} on PHP ${{ matrix.php }} + name: Tests for Deployer ${{ matrix.deployer }} on PHP ${{ matrix.php }} steps: # Checkout the repository @@ -37,7 +37,8 @@ jobs: # Install specific Deployer version - name: Install Deployer version - run: composer require --no-update deployer/deployer:"${{ matrix.deployer }}" + # Install latest patch of the specified version. + run: composer require --no-update deployer/deployer:"${{ matrix.deployer }}.*" # Run PHPStan - name: Run PHPStan diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c903491..29b1f1a 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -13,7 +13,7 @@ // Set up test environment putenv('DEPLOYER_LOCAL_WORKER=true'); -define('__FIXTURES__', __DIR__ . '/fixtures'); +define('__FIXTURES__', __DIR__ . '/Fixtures'); define('__TEMP_DIR__', sys_get_temp_dir() . '/deployer-wordpress'); // Create temp directory if it doesn't exist From de29f12c2b2ec24b89dca51e2c07accd61d94c7c Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 12:32:12 +0100 Subject: [PATCH 16/36] add comment to actions --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe43552..86a1ad3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,7 +40,7 @@ jobs: # Install latest patch of the specified version. run: composer require --no-update deployer/deployer:"${{ matrix.deployer }}.*" - # Run PHPStan + # Run PHPStan so we check compatibility with the specified Deployer version. - name: Run PHPStan run: composer run-script phpstan From 3bd3a6688e15f666a3b11fe9337b5e10ac47751b Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 13:11:02 +0100 Subject: [PATCH 17/36] add Composer and WPCLI tests, fix sudo prefix --- .gitignore | 3 +- src/Composer.php | 9 +- src/WPCLI.php | 8 +- tests/Integration/ComposerIntegrationTest.php | 229 ++++++++++++++++++ tests/Integration/IntegrationTestCase.php | 9 +- tests/Integration/WpCliIntegrationTest.php | 154 ++++++++++++ 6 files changed, 400 insertions(+), 12 deletions(-) create mode 100644 tests/Integration/ComposerIntegrationTest.php create mode 100644 tests/Integration/WpCliIntegrationTest.php diff --git a/.gitignore b/.gitignore index 8aca3aa..e493d4d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /.idea .DS_STORE .phpunit.cache -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +/coverage \ No newline at end of file diff --git a/src/Composer.php b/src/Composer.php index ddf56e3..c07b759 100644 --- a/src/Composer.php +++ b/src/Composer.php @@ -60,14 +60,13 @@ public static function install( string $binaryName = 'composer.phar', bool $sudo = false ): string { - $sudoCommand = $sudo ? 'sudo ' : ''; + $sudoPrefix = $sudo ? 'sudo ' : ''; - run("mkdir -p $installPath"); - run("cd $installPath && curl -sS " . self::INSTALLER_DOWNLOAD . " | {{bin/php}}"); - run('mv {{deploy_path}}/composer.phar {{deploy_path}}/.dep/composer.phar'); + run($sudoPrefix . "mkdir -p $installPath"); + run($sudoPrefix . "cd $installPath && curl -sS " . self::INSTALLER_DOWNLOAD . " | {{bin/php}}"); if ($binaryName !== 'composer.phar') { - run("$sudoCommand mv $installPath/composer.phar $installPath/$binaryName"); + run($sudoPrefix . "mv $installPath/composer.phar $installPath/$binaryName"); } return "$installPath/$binaryName"; diff --git a/src/WPCLI.php b/src/WPCLI.php index 8fafec1..1201907 100644 --- a/src/WPCLI.php +++ b/src/WPCLI.php @@ -11,7 +11,7 @@ */ class WPCLI { - private const INSTALLER_DOWNLOAD = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar'; + private const BINARY_DOWNLOAD = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar'; private const DEFAULT_PATH = '{{release_or_current_path}}'; /** @@ -64,11 +64,11 @@ public static function install(string $installPath, string $binaryName = 'wp-cli { $sudoPrefix = $sudo ? 'sudo ' : ''; - run("mkdir -p $installPath"); - run("cd $installPath && curl -sS -O " . self::INSTALLER_DOWNLOAD); + run($sudoPrefix . "mkdir -p $installPath"); + run($sudoPrefix . "cd $installPath && curl -sS " . self::BINARY_DOWNLOAD . " -o wp-cli.phar"); if ($binaryName !== 'wp-cli.phar') { - run("$sudoPrefix mv $installPath/wp-cli.phar $installPath/$binaryName"); + run($sudoPrefix . "mv $installPath/wp-cli.phar $installPath/$binaryName"); } return "$installPath/$binaryName"; diff --git a/tests/Integration/ComposerIntegrationTest.php b/tests/Integration/ComposerIntegrationTest.php new file mode 100644 index 0000000..a6e6cb4 --- /dev/null +++ b/tests/Integration/ComposerIntegrationTest.php @@ -0,0 +1,229 @@ +processRunnerMock = $this->createMock(ProcessRunner::class); + $this->sshClientMock = $this->createMock(Client::class); + $this->outputMock = $this->createMock(OutputInterface::class); + + // Set them in the Deployer container + $this->deployer['processRunner'] = $this->processRunnerMock; + $this->deployer['sshClient'] = $this->sshClientMock; + $this->deployer['output'] = $this->outputMock; + + // Set default composer configuration + $this->deployer->config->set('composer_action', 'install'); + $this->deployer->config->set('composer_options', '--no-dev --no-interaction'); + $this->deployer->config->set('bin/composer', 'composer'); + $this->deployer->config->set('bin/php', 'php'); + $this->deployer->config->set('deploy_path', '/var/www'); + } + + public function testRunDefault(): void + { + $path = '/var/www/html'; + $expectedCommand = "cd $path && composer install --no-dev --no-interaction "; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('Composer output'); + + $result = Composer::runDefault($path); + $this->assertEquals('Composer output', $result); + } + + public function testRunCommand(): void + { + $path = '/var/www/html'; + $command = 'require'; + $arguments = 'package/name:1.0.0'; + $expectedCommand = "cd $path && composer require package/name:1.0.0 "; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('Composer output'); + + $result = Composer::runCommand($path, $command, $arguments); + $this->assertEquals('Composer output', $result); + } + + public function testRunScript(): void + { + $path = '/var/www/html'; + $script = 'post-install-cmd'; + $arguments = '--env=prod'; + $expectedCommand = "cd $path && composer run-script post-install-cmd --env=prod "; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('Script output'); + + $result = Composer::runScript($path, $script, $arguments); + $this->assertEquals('Script output', $result); + } + + public function testInstall(): void + { + $installPath = '/usr/local/bin'; + $binaryName = 'composer'; + $expectedCommands = [ + "sudo mkdir -p $installPath", + "sudo cd $installPath && curl -sS https://getcomposer.org/installer | php", + "sudo mv $installPath/composer.phar $installPath/$binaryName" + ]; + + // Set up expectations for each command in sequence + $this->processRunnerMock + ->expects($this->exactly(3)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($expectedCommands) { + static $index = 0; + $this->assertEquals($expectedCommands[$index], $command); + $index++; + return 'Installation output'; + }); + + $result = Composer::install($installPath, $binaryName, true); + $this->assertEquals("$installPath/$binaryName", $result); + } + + public function testInstallWithoutSudo(): void + { + $installPath = '/usr/local/bin'; + $binaryName = 'composer'; + $expectedCommands = [ + "mkdir -p $installPath", + "cd $installPath && curl -sS https://getcomposer.org/installer | php", + "mv $installPath/composer.phar $installPath/$binaryName" + ]; + + // Set up expectations for each command in sequence + $this->processRunnerMock + ->expects($this->exactly(3)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($expectedCommands) { + static $index = 0; + $this->assertEquals($expectedCommands[$index], $command); + $index++; + return 'Installation output'; + }); + + $result = Composer::install($installPath, $binaryName, false); + $this->assertEquals("$installPath/$binaryName", $result); + } + + public function testInstallWithDefaultBinaryName(): void + { + $installPath = '/usr/local/bin'; + $expectedCommands = [ + "mkdir -p $installPath", + "cd $installPath && curl -sS https://getcomposer.org/installer | php" + ]; + + // Set up expectations for each command in sequence + $this->processRunnerMock + ->expects($this->exactly(2)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($expectedCommands) { + static $index = 0; + $this->assertEquals($expectedCommands[$index], $command); + $index++; + return 'Installation output'; + }); + + $result = Composer::install($installPath); + $this->assertEquals("$installPath/composer.phar", $result); + } + + public function testRunCommandWithVerbosity(): void + { + $path = '/var/www/html'; + $command = 'require'; + $arguments = 'package/name:1.0.0'; + + // Test with verbose output + $this->outputMock->method('isVerbose')->willReturn(true); + $this->outputMock->method('isVeryVerbose')->willReturn(false); + $this->outputMock->method('isDebug')->willReturn(false); + + $expectedCommand = "cd $path && composer require package/name:1.0.0 -v"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('Composer output'); + + $result = Composer::runCommand($path, $command, $arguments); + $this->assertEquals('Composer output', $result); + } + + public function testRunCommandWithVeryVerboseOutput(): void + { + $path = '/var/www/html'; + $command = 'require'; + $arguments = 'package/name:1.0.0'; + + // Test with very verbose output + $this->outputMock->method('isVerbose')->willReturn(false); + $this->outputMock->method('isVeryVerbose')->willReturn(true); + $this->outputMock->method('isDebug')->willReturn(false); + + $expectedCommand = "cd $path && composer require package/name:1.0.0 -vv"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('Composer output'); + + $result = Composer::runCommand($path, $command, $arguments); + $this->assertEquals('Composer output', $result); + } + + public function testRunCommandWithDebugOutput(): void + { + $path = '/var/www/html'; + $command = 'require'; + $arguments = 'package/name:1.0.0'; + + // Test with debug output + $this->outputMock->method('isVerbose')->willReturn(false); + $this->outputMock->method('isVeryVerbose')->willReturn(false); + $this->outputMock->method('isDebug')->willReturn(true); + + $expectedCommand = "cd $path && composer require package/name:1.0.0 -vvv"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('Composer output'); + + $result = Composer::runCommand($path, $command, $arguments); + $this->assertEquals('Composer output', $result); + } +} diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index bbddd23..d49102b 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -6,6 +6,7 @@ use Deployer\Host\Host; use Deployer\Host\Localhost; use Deployer\Task\Context; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\Input; @@ -14,8 +15,8 @@ abstract class IntegrationTestCase extends TestCase { protected Deployer $deployer; - protected Input $input; - protected Output $output; + protected MockObject|Input $input; + protected MockObject|Output $output; protected Host $host; protected function setUp(): void @@ -36,6 +37,10 @@ protected function setUp(): void // Create a localhost instance for testing $this->host = new Localhost(); + $this->host->set('deploy_path', '/var/www'); + $this->host->set('bin/wp', 'wp'); + $this->host->set('release_or_current_path', '/var/www/current'); + $this->deployer->hosts->set('localhost', $this->host); // Push a new context with our host Context::push(new Context($this->host)); diff --git a/tests/Integration/WpCliIntegrationTest.php b/tests/Integration/WpCliIntegrationTest.php new file mode 100644 index 0000000..0dfb0b4 --- /dev/null +++ b/tests/Integration/WpCliIntegrationTest.php @@ -0,0 +1,154 @@ +processRunnerMock = $this->createMock(\Deployer\Component\ProcessRunner\ProcessRunner::class); + $this->deployer['processRunner'] = $this->processRunnerMock; + } + + public function testRunCommand(): void + { + $path = '/var/www/html'; + $command = 'post list'; + $arguments = '--format=table'; + $expectedCommand = "cd $path && wp post list --format=table"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('WP-CLI output'); + + WPCLI::runCommand($command, $path, $arguments); + } + + public function testRunCommandWithoutPath(): void + { + $command = 'post list'; + $arguments = '--format=table'; + $expectedCommand = "wp post list --format=table"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('WP-CLI output'); + + WPCLI::runCommand($command, null, $arguments); + } + + public function testRunCommandLocally(): void + { + // Note: We can't test for the host object because runLocally creates a new host instance + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with( + $this->anything(), // Host object is not reliable as runLocally creates a new instance + 'cd /var/www/current && wp post list --format=table ', + [] + ); + + WPCLI::runCommandLocally('post list --format=table'); + } + + public function testRunCommandLocallyWithoutPath(): void + { + // Note: We can't test for the host object because runLocally creates a new host instance + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with( + $this->anything(), // Host object is not reliable as runLocally creates a new instance + 'wp post list --format=table ', + [] + ); + + WPCLI::runCommandLocally('post list --format=table', false); + } + + public function testInstall(): void + { + $installPath = '/usr/local/bin'; + $binaryName = 'wp'; + $expectedCommands = [ + "sudo mkdir -p $installPath", + "sudo cd $installPath && curl -sS https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o wp-cli.phar", + "sudo mv $installPath/wp-cli.phar $installPath/$binaryName" + ]; + + // Set up expectations for each command in sequence + $this->processRunnerMock + ->expects($this->exactly(3)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($expectedCommands) { + static $index = 0; + $this->assertEquals($expectedCommands[$index], $command); + $index++; + return 'Installation output'; + }); + + $result = WPCLI::install($installPath, $binaryName, true); + $this->assertEquals("$installPath/$binaryName", $result); + } + + public function testInstallWithoutSudo(): void + { + $installPath = '/usr/local/bin'; + $binaryName = 'wp'; + $expectedCommands = [ + "mkdir -p $installPath", + "cd $installPath && curl -sS https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o wp-cli.phar", + "mv $installPath/wp-cli.phar $installPath/$binaryName" + ]; + + // Set up expectations for each command in sequence + $this->processRunnerMock + ->expects($this->exactly(3)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($expectedCommands) { + static $index = 0; + $this->assertEquals($expectedCommands[$index], $command); + $index++; + return 'Installation output'; + }); + + $result = WPCLI::install($installPath, $binaryName, false); + $this->assertEquals("$installPath/$binaryName", $result); + } + + public function testInstallWithDefaultBinaryName(): void + { + $installPath = '/usr/local/bin'; + $expectedCommands = [ + "mkdir -p $installPath", + "cd $installPath && curl -sS https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o wp-cli.phar" + ]; + + // Set up expectations for each command in sequence + $this->processRunnerMock + ->expects($this->exactly(2)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($expectedCommands) { + static $index = 0; + $this->assertEquals($expectedCommands[$index], $command); + $index++; + return 'Installation output'; + }); + + $result = WPCLI::install($installPath); + $this->assertEquals("$installPath/wp-cli.phar", $result); + } +} From 850d0fbd2c513d1c607dc7ec25db8f1cebf076c3 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 14:45:14 +0100 Subject: [PATCH 18/36] add Integration tests for all classes in src --- composer.json | 1 + src/NPM.php | 11 +- tests/Integration/ComposerIntegrationTest.php | 63 ++---- tests/Integration/FilesIntegrationTest.php | 141 +++++++++++++ .../Integration/LocalhostIntegrationTest.php | 40 ++++ tests/Integration/NPMIntegrationTest.php | 196 ++++++++++++++++++ tests/Integration/UtilsIntegrationTest.php | 44 ++++ 7 files changed, 446 insertions(+), 50 deletions(-) create mode 100644 tests/Integration/FilesIntegrationTest.php create mode 100644 tests/Integration/LocalhostIntegrationTest.php create mode 100644 tests/Integration/NPMIntegrationTest.php create mode 100644 tests/Integration/UtilsIntegrationTest.php diff --git a/composer.json b/composer.json index 9510ed7..940dead 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "phpstan": "vendor/bin/phpstan analyse --memory-limit 1G", "lint": "find . -type f -name '*.php' -not -path './vendor/*' -exec php -l {} \\;", "test": "vendor/bin/phpunit", + "test:coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --filter 'Gaambo\\\\DeployerWordpress\\\\'", "precommit": [ "@lint", "@phpcs", diff --git a/src/NPM.php b/src/NPM.php index a3ac3bc..d43c3e1 100644 --- a/src/NPM.php +++ b/src/NPM.php @@ -35,7 +35,16 @@ public static function runCommand(string $path, string $action, string $argument { $verbosityArgument = Utils::getVerbosityArgument(); $verbosityArgument = str_replace('v', 'd', $verbosityArgument); // npm takes d for verbosity argument - return run("cd $path && {{bin/npm}} $action $arguments $verbosityArgument"); + + $command = "cd $path && {{bin/npm}} $action"; + if ($arguments !== '') { + $command .= " $arguments"; + } + if ($verbosityArgument !== '') { + $command .= " $verbosityArgument"; + } + + return run($command); } /** diff --git a/tests/Integration/ComposerIntegrationTest.php b/tests/Integration/ComposerIntegrationTest.php index a6e6cb4..ed485a0 100644 --- a/tests/Integration/ComposerIntegrationTest.php +++ b/tests/Integration/ComposerIntegrationTest.php @@ -158,18 +158,20 @@ public function testInstallWithDefaultBinaryName(): void $this->assertEquals("$installPath/composer.phar", $result); } - public function testRunCommandWithVerbosity(): void + /** + * @dataProvider verbosityProvider + */ + public function testRunCommandWithVerbosity(bool $isVerbose, bool $isVeryVerbose, bool $isDebug, string $verbosityFlag): void { $path = '/var/www/html'; $command = 'require'; $arguments = 'package/name:1.0.0'; - // Test with verbose output - $this->outputMock->method('isVerbose')->willReturn(true); - $this->outputMock->method('isVeryVerbose')->willReturn(false); - $this->outputMock->method('isDebug')->willReturn(false); + $this->outputMock->method('isVerbose')->willReturn($isVerbose); + $this->outputMock->method('isVeryVerbose')->willReturn($isVeryVerbose); + $this->outputMock->method('isDebug')->willReturn($isDebug); - $expectedCommand = "cd $path && composer require package/name:1.0.0 -v"; + $expectedCommand = "cd $path && composer require package/name:1.0.0 $verbosityFlag"; $this->processRunnerMock ->expects($this->once()) @@ -181,49 +183,12 @@ public function testRunCommandWithVerbosity(): void $this->assertEquals('Composer output', $result); } - public function testRunCommandWithVeryVerboseOutput(): void + public static function verbosityProvider(): array { - $path = '/var/www/html'; - $command = 'require'; - $arguments = 'package/name:1.0.0'; - - // Test with very verbose output - $this->outputMock->method('isVerbose')->willReturn(false); - $this->outputMock->method('isVeryVerbose')->willReturn(true); - $this->outputMock->method('isDebug')->willReturn(false); - - $expectedCommand = "cd $path && composer require package/name:1.0.0 -vv"; - - $this->processRunnerMock - ->expects($this->once()) - ->method('run') - ->with($this->host, $expectedCommand) - ->willReturn('Composer output'); - - $result = Composer::runCommand($path, $command, $arguments); - $this->assertEquals('Composer output', $result); - } - - public function testRunCommandWithDebugOutput(): void - { - $path = '/var/www/html'; - $command = 'require'; - $arguments = 'package/name:1.0.0'; - - // Test with debug output - $this->outputMock->method('isVerbose')->willReturn(false); - $this->outputMock->method('isVeryVerbose')->willReturn(false); - $this->outputMock->method('isDebug')->willReturn(true); - - $expectedCommand = "cd $path && composer require package/name:1.0.0 -vvv"; - - $this->processRunnerMock - ->expects($this->once()) - ->method('run') - ->with($this->host, $expectedCommand) - ->willReturn('Composer output'); - - $result = Composer::runCommand($path, $command, $arguments); - $this->assertEquals('Composer output', $result); + return [ + 'verbose' => [true, false, false, '-v'], + 'very verbose' => [false, true, false, '-vv'], + 'debug' => [false, false, true, '-vvv'], + ]; } } diff --git a/tests/Integration/FilesIntegrationTest.php b/tests/Integration/FilesIntegrationTest.php new file mode 100644 index 0000000..83ff67d --- /dev/null +++ b/tests/Integration/FilesIntegrationTest.php @@ -0,0 +1,141 @@ +processRunnerMock = $this->createMock(ProcessRunner::class); + $this->sshClientMock = $this->createMock(Client::class); + $this->outputMock = $this->createMock(OutputInterface::class); + $this->rsyncMock = $this->createMock(\Deployer\Utility\Rsync::class); + + // Set them in the Deployer container + $this->deployer['processRunner'] = $this->processRunnerMock; + $this->deployer['sshClient'] = $this->sshClientMock; + $this->deployer['output'] = $this->outputMock; + $this->deployer['rsync'] = $this->rsyncMock; + + // Set default configuration + $this->host->set('current_path', '/var/www/current'); + $this->host->set('release_or_current_path', '/var/www/current'); + $this->host->set('zip_options', '--exclude=*.zip'); + } + + public function testPushFiles(): void + { + $localPath = 'wp-content/themes/my-theme'; + $remotePath = 'wp-content/themes'; + $rsyncOptions = ['--exclude=*.log']; + + // Set up expectations for upload + $this->rsyncMock + ->expects($this->once()) + ->method('call') + ->with( + $this->host, + '/var/www/current/wp-content/themes/my-theme/', + '/var/www/current/wp-content/themes/', + ['options' => $rsyncOptions] + ); + + Files::pushFiles($localPath, $remotePath, $rsyncOptions); + } + + public function testPullFiles(): void + { + $remotePath = 'wp-content/uploads'; + $localPath = 'wp-content/uploads'; + $rsyncOptions = ['--exclude=*.tmp']; + + // Set up expectations for download + $this->rsyncMock + ->expects($this->once()) + ->method('call') + ->with( + $this->host, + '/var/www/current/wp-content/uploads/', + '/var/www/current/wp-content/uploads/', + ['options' => $rsyncOptions] + ); + + Files::pullFiles($remotePath, $localPath, $rsyncOptions); + } + + public function testZipFilesWithTrailingSlash(): void + { + $dir = '/var/www/current/wp-content/uploads/'; + $backupDir = '/var/www/backups'; + $filename = 'uploads'; + + // Set up expectations for zip command + $this->processRunnerMock + ->expects($this->exactly(2)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($backupDir, $filename) { + static $callNumber = 0; + $callNumber++; + + switch ($callNumber) { + case 1: + $this->assertEquals("mkdir -p $backupDir", $command); + return 'Mkdir output'; + case 2: + $this->assertStringContainsString("cd /var/www/current/wp-content/uploads/ && zip -r ", $command); + $this->assertStringContainsString(" . --exclude=*.zip && mv ", $command); + $this->assertStringContainsString(" $backupDir/", $command); + return 'Zip output'; + } + }); + + $result = Files::zipFiles($dir, $backupDir, $filename); + $this->assertStringContainsString("$backupDir/{$filename}_", $result); + $this->assertStringEndsWith('.zip', $result); + } + + public function testZipFilesWithoutTrailingSlash(): void + { + $dir = '/var/www/current/wp-content/uploads'; + $backupDir = '/var/www/backups'; + $filename = 'uploads'; + + // Set up expectations for zip command + $this->processRunnerMock + ->expects($this->exactly(2)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($backupDir, $filename) { + static $callNumber = 0; + $callNumber++; + + switch ($callNumber) { + case 1: + $this->assertEquals("mkdir -p $backupDir", $command); + return 'Mkdir output'; + case 2: + $this->assertStringContainsString("cd /var/www/current/wp-content && zip -r ", $command); + $this->assertStringContainsString(" uploads --exclude=*.zip && mv ", $command); + $this->assertStringContainsString(" $backupDir/", $command); + return 'Zip output'; + } + }); + + $result = Files::zipFiles($dir, $backupDir, $filename); + $this->assertStringContainsString("$backupDir/{$filename}_", $result); + $this->assertStringEndsWith('.zip', $result); + } +} \ No newline at end of file diff --git a/tests/Integration/LocalhostIntegrationTest.php b/tests/Integration/LocalhostIntegrationTest.php new file mode 100644 index 0000000..cc1158f --- /dev/null +++ b/tests/Integration/LocalhostIntegrationTest.php @@ -0,0 +1,40 @@ +host->set('test_key', 'test_value'); + + // Test getting configuration + $value = Localhost::getConfig('test_key'); + $this->assertEquals('test_value', $value); + } + + public function testGetConfigWithNonExistentKey(): void + { + // Test getting non-existent configuration + $value = Localhost::getConfig('non_existent_key'); + $this->assertNull($value); + } + + public function testGet(): void + { + // Test getting localhost instance + $host = Localhost::get(); + + // Verify it's the same instance we set up in IntegrationTestCase + $this->assertSame($this->host, $host); + + // Verify it has the expected configuration + $this->assertEquals('/var/www', $host->get('deploy_path')); + $this->assertEquals('wp', $host->get('bin/wp')); + $this->assertEquals('/var/www/current', $host->get('release_or_current_path')); + } +} \ No newline at end of file diff --git a/tests/Integration/NPMIntegrationTest.php b/tests/Integration/NPMIntegrationTest.php new file mode 100644 index 0000000..5618fb4 --- /dev/null +++ b/tests/Integration/NPMIntegrationTest.php @@ -0,0 +1,196 @@ +processRunnerMock = $this->createMock(ProcessRunner::class); + $this->sshClientMock = $this->createMock(Client::class); + $this->outputMock = $this->createMock(OutputInterface::class); + + // Set them in the Deployer container + $this->deployer['processRunner'] = $this->processRunnerMock; + $this->deployer['sshClient'] = $this->sshClientMock; + $this->deployer['output'] = $this->outputMock; + + // Set default npm configuration + $this->deployer->config->set('bin/npm', 'npm'); + } + + public function testRunScript(): void + { + $path = '/var/www/html'; + $script = 'build'; + $arguments = '--env=production'; + $expectedCommand = "cd $path && npm run-script $script $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('NPM output'); + + $result = NPM::runScript($path, $script, $arguments); + $this->assertEquals('NPM output', $result); + $this->addToAssertionCount(1); // Count the mock expectation as an assertion + } + + public function testRunCommand(): void + { + $path = '/var/www/html'; + $action = 'install'; + $arguments = '--save-dev package-name'; + $expectedCommand = "cd $path && npm $action $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('NPM output'); + + $result = NPM::runCommand($path, $action, $arguments); + $this->assertEquals('NPM output', $result); + $this->addToAssertionCount(1); // Count the mock expectation as an assertion + } + + public function testRunInstall(): void + { + $path = '/var/www/html'; + $arguments = '--save-dev'; + $expectedCommand = "cd $path && npm install $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('NPM output'); + + $result = NPM::runInstall($path, $arguments); + $this->assertEquals('NPM output', $result); + $this->addToAssertionCount(1); // Count the mock expectation as an assertion + } + + public function testRunInstallWithPreviousRelease(): void + { + $path = '/var/www/html'; + + // Set previous_release config + $this->deployer->config->set('previous_release', '/var/www/releases/1'); + + // ProcessRunner will be called 3 times: + // 1. test command to check if node_modules exists (wrapped in bash-if) + // 2. cp command to copy node_modules + // 3. npm install command + $this->processRunnerMock + ->expects($this->exactly(3)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($path) { + static $callNumber = 0; + $callNumber++; + + switch ($callNumber) { + case 1: + // test() wraps the command in a bash-if and checks for success via echo + $this->assertStringContainsString('if [ -d /var/www/releases/1/node_modules ]; then echo +', $command); + // Extract the random value from the command string + if (preg_match('/echo \+([a-z]+);/', $command, $matches)) { + return '+' . $matches[1]; + } + throw new \RuntimeException('Could not extract random value from command'); + case 2: + $this->assertEquals("cp -R /var/www/releases/1/node_modules $path", $command); + return 'Copy output'; + case 3: + $this->assertEquals("cd $path && npm install", $command); + return 'NPM output'; + } + }); + + $result = NPM::runInstall($path); + $this->assertEquals('NPM output', $result); + $this->addToAssertionCount(3); // Count the mock callback assertions + } + + public function testRunInstallWithPreviousReleaseNoNodeModules(): void + { + $path = '/var/www/html'; + + // Set previous_release config + $this->deployer->config->set('previous_release', '/var/www/releases/1'); + + // ProcessRunner will be called 2 times: + // 1. test command to check if node_modules exists (wrapped in bash-if, returns empty) + // 2. npm install command + $this->processRunnerMock + ->expects($this->exactly(2)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($path) { + static $callNumber = 0; + $callNumber++; + + switch ($callNumber) { + case 1: + // test() wraps the command in a bash-if and checks for success via echo + $this->assertStringContainsString('if [ -d /var/www/releases/1/node_modules ]; then echo +', $command); + return '0'; // False value. + case 2: + $this->assertEquals("cd $path && npm install", $command); + return 'NPM output'; + } + }); + + $result = NPM::runInstall($path); + $this->assertEquals('NPM output', $result); + $this->addToAssertionCount(2); // Count the mock callback assertions + } + + /** + * @dataProvider verbosityProvider + */ + public function testRunCommandWithVerbosity(bool $isVerbose, bool $isVeryVerbose, bool $isDebug, string $verbosityFlag): void + { + $path = '/var/www/html'; + $action = 'install'; + $arguments = '--save-dev'; + + $this->outputMock->method('isVerbose')->willReturn($isVerbose); + $this->outputMock->method('isVeryVerbose')->willReturn($isVeryVerbose); + $this->outputMock->method('isDebug')->willReturn($isDebug); + + $expectedCommand = "cd $path && npm $action $arguments $verbosityFlag"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('NPM output'); + + $result = NPM::runCommand($path, $action, $arguments); + $this->assertEquals('NPM output', $result); + $this->addToAssertionCount(1); // Count the mock expectation as an assertion + } + + public static function verbosityProvider(): array + { + return [ + 'verbose' => [true, false, false, '-d'], + 'very verbose' => [false, true, false, '-dd'], + 'debug' => [false, false, true, '-ddd'], + ]; + } +} \ No newline at end of file diff --git a/tests/Integration/UtilsIntegrationTest.php b/tests/Integration/UtilsIntegrationTest.php new file mode 100644 index 0000000..5c5ac2d --- /dev/null +++ b/tests/Integration/UtilsIntegrationTest.php @@ -0,0 +1,44 @@ +outputMock = $this->createMock(OutputInterface::class); + $this->deployer['output'] = $this->outputMock; + } + + /** + * @dataProvider verbosityProvider + */ + public function testGetVerbosityArgument(bool $isVerbose, bool $isVeryVerbose, bool $isDebug, string $expectedArgument): void + { + $this->outputMock->method('isVerbose')->willReturn($isVerbose); + $this->outputMock->method('isVeryVerbose')->willReturn($isVeryVerbose); + $this->outputMock->method('isDebug')->willReturn($isDebug); + + $result = Utils::getVerbosityArgument(); + $this->assertEquals($expectedArgument, $result); + } + + public static function verbosityProvider(): array + { + return [ + 'normal output' => [false, false, false, ''], + 'verbose output' => [true, false, false, '-v'], + 'very verbose output' => [false, true, false, '-vv'], + 'debug output' => [false, false, true, '-vvv'], + ]; + } +} \ No newline at end of file From a6ecb364464e3e907e28d984567f66a8dfec48fa Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 16:14:12 +0100 Subject: [PATCH 19/36] test more edge cases --- src/Composer.php | 12 +- src/Rsync.php | 72 +++--- src/Utils.php | 27 +++ tests/Integration/ComposerIntegrationTest.php | 42 +++- tests/Integration/NPMIntegrationTest.php | 153 +++++++++++++ tests/Integration/RsyncIntegrationTest.php | 216 ++++++++++++++++++ tests/Integration/WpCliIntegrationTest.php | 195 ++++++++++++++++ 7 files changed, 685 insertions(+), 32 deletions(-) diff --git a/src/Composer.php b/src/Composer.php index c07b759..67b27ad 100644 --- a/src/Composer.php +++ b/src/Composer.php @@ -31,7 +31,17 @@ public static function runDefault(string $path): string */ public static function runCommand(string $path, string $command, string $arguments = ''): string { - return run("cd $path && {{bin/composer}} $command $arguments " . Utils::getVerbosityArgument()); + $verbosityArgument = Utils::getVerbosityArgument(); + + $runCommand = "cd $path && {{bin/composer}} $command"; + if ($arguments !== '') { + $runCommand .= " $arguments"; + } + if ($verbosityArgument !== '') { + $runCommand .= " $verbosityArgument"; + } + + return run($runCommand); } /** diff --git a/src/Rsync.php b/src/Rsync.php index fbbb008..69007cf 100644 --- a/src/Rsync.php +++ b/src/Rsync.php @@ -21,6 +21,20 @@ */ class Rsync { + public const DEFAULT_CONFIG = [ + 'exclude' => [ + '.git', + 'deploy.php', + ], + 'exclude-file' => false, + 'include' => [], + 'include-file' => false, + 'filter' => [], + 'filter-file' => false, + 'filter-perdir' => false, + 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterward + ]; + /** * Builds a comprehensive array of rsync command options * by merging default (set in `rsync` config) and custom configurations. @@ -46,41 +60,43 @@ class Rsync */ public static function buildOptionsArray(array $config = []): array { + // Get default config or use fallback $defaultConfig = get('rsync'); if (!$defaultConfig || !is_array($defaultConfig)) { - $defaultConfig = [ - 'exclude' => [ - '.git', - 'deploy.php', - ], - 'exclude-file' => false, - 'include' => [], - 'include-file' => false, - 'filter' => [], - 'filter-file' => false, - 'filter-perdir' => false, - 'options' => ['delete-after'], // needed so deployfilter files are send and delete is checked afterward - ]; + $defaultConfig = [...self::DEFAULT_CONFIG]; } - $mergedConfig = array_merge($defaultConfig, $config); - - // Filter out empty strings from arrays before building options - $mergedConfig['options'] = array_filter($mergedConfig['options']); - $mergedConfig['exclude'] = array_filter($mergedConfig['exclude']); - $mergedConfig['include'] = array_filter($mergedConfig['include']); - $mergedConfig['filter'] = array_filter($mergedConfig['filter']); - - $options = array_merge( - self::buildOptions($mergedConfig['options']), - self::buildIncludes($mergedConfig['include'], $mergedConfig['include-file']), - self::buildExcludes($mergedConfig['exclude'], $mergedConfig['exclude-file']), - self::buildFilter($mergedConfig['filter'], $mergedConfig['filter-file'], $mergedConfig['filter-perdir']) - ); + $options = array_key_exists('options', $config) ? $config['options'] : ($defaultConfig['options'] ?? []); + $options = Utils::parseStringArray((array) $options); + $exclude = array_key_exists('exclude', $config) ? $config['exclude'] : ($defaultConfig['exclude'] ?? []); + $exclude = Utils::parseStringArray((array) $exclude); + $excludeFile = array_key_exists('exclude-file', $config) ? + $config['exclude-file'] : ($defaultConfig['exclude-file'] ?? null); + $excludeFile = Utils::parseStringOrNull($excludeFile); + $include = array_key_exists('include', $config) ? + $config['include'] : ($defaultConfig['include'] ?? []); + $include = Utils::parseStringArray((array) $include); + $includeFile = array_key_exists('include-file', $config) ? + $config['include-file'] : ($defaultConfig['include-file'] ?? null); + $includeFile = Utils::parseStringOrNull($includeFile); + $filter = array_key_exists('filter', $config) ? $config['filter'] : ($defaultConfig['filter'] ?? []); + $filter = Utils::parseStringArray((array) $filter); + $filterFile = array_key_exists('filter-file', $config) ? + $config['filter-file'] : ($defaultConfig['filter-file'] ?? null); + $filterFile = Utils::parseStringOrNull($filterFile); + $filterPerDir = array_key_exists('filter-perdir', $config) ? + $config['filter-perdir'] : ($defaultConfig['filter-perdir'] ?? null); + $filterPerDir = Utils::parseStringOrNull($filterPerDir); + + // Build options + $options = self::buildOptions($options); + $includes = self::buildIncludes($include, $includeFile); + $excludes = self::buildExcludes($exclude, $excludeFile); + $filters = self::buildFilter($filter, $filterFile, $filterPerDir); // remove empty strings because they break rsync // because Rsync class uses escapeshellarg - return array_filter($options); + return array_filter([...$options, ...$includes, ...$excludes, ...$filters]); } /** diff --git a/src/Utils.php b/src/Utils.php index 88b97ee..66554ac 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -28,4 +28,31 @@ public static function getVerbosityArgument(): string return $verbosityArgument; } + + /** + * Parses an array of mixed values and returns an array of strings. + * @param array $array + * @return array + */ + public static function parseStringArray(array $array): array + { + return array_values( + array_filter( + array_map( + fn($value) => is_string($value) ? $value : null, + $array + ), + ) + ); + } + + /** + * Parses a mixed value and returns a string or null if the value is not a string. + * @param mixed $string + * @return string|null + */ + public static function parseStringOrNull(mixed $string): ?string + { + return is_string($string) ? $string : null; + } } diff --git a/tests/Integration/ComposerIntegrationTest.php b/tests/Integration/ComposerIntegrationTest.php index ed485a0..a77fdc5 100644 --- a/tests/Integration/ComposerIntegrationTest.php +++ b/tests/Integration/ComposerIntegrationTest.php @@ -4,6 +4,7 @@ use Deployer\Component\ProcessRunner\ProcessRunner; use Deployer\Component\Ssh\Client; +use Deployer\Exception\ConfigurationException; use Gaambo\DeployerWordpress\Composer; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Console\Output\OutputInterface; @@ -39,7 +40,7 @@ protected function setUp(): void public function testRunDefault(): void { $path = '/var/www/html'; - $expectedCommand = "cd $path && composer install --no-dev --no-interaction "; + $expectedCommand = "cd $path && composer install --no-dev --no-interaction"; $this->processRunnerMock ->expects($this->once()) @@ -56,7 +57,7 @@ public function testRunCommand(): void $path = '/var/www/html'; $command = 'require'; $arguments = 'package/name:1.0.0'; - $expectedCommand = "cd $path && composer require package/name:1.0.0 "; + $expectedCommand = "cd $path && composer require package/name:1.0.0"; $this->processRunnerMock ->expects($this->once()) @@ -73,7 +74,7 @@ public function testRunScript(): void $path = '/var/www/html'; $script = 'post-install-cmd'; $arguments = '--env=prod'; - $expectedCommand = "cd $path && composer run-script post-install-cmd --env=prod "; + $expectedCommand = "cd $path && composer run-script post-install-cmd --env=prod"; $this->processRunnerMock ->expects($this->once()) @@ -191,4 +192,39 @@ public static function verbosityProvider(): array 'debug' => [false, false, true, '-vvv'], ]; } + + public function testRunCommandWithCustomComposerBinary(): void + { + $path = '/var/www/html'; + $action = 'install'; + $arguments = '--no-dev'; + $expectedCommand = "cd $path && /usr/local/bin/composer $action $arguments"; + + // Set custom composer binary path + $this->deployer->config->set('bin/composer', '/usr/local/bin/composer'); + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('Composer output'); + + $result = Composer::runCommand($path, $action, $arguments); + $this->assertEquals('Composer output', $result); + } + + public function testRunCommandWithInvalidComposerBinary(): void + { + $path = '/var/www/html'; + $action = 'install'; + $arguments = '--no-dev'; + $expectedCommand = "cd $path && composer $action $arguments"; + + // Set invalid composer binary (should fall back to default) + $this->deployer->config->set('bin/composer', null); + + $this->expectException(ConfigurationException::class); + $this->expectExceptionMessage('Config option "bin/composer" does not exist'); + $result = Composer::runCommand($path, $action, $arguments); + } } diff --git a/tests/Integration/NPMIntegrationTest.php b/tests/Integration/NPMIntegrationTest.php index 5618fb4..f065c1e 100644 --- a/tests/Integration/NPMIntegrationTest.php +++ b/tests/Integration/NPMIntegrationTest.php @@ -4,6 +4,7 @@ use Deployer\Component\ProcessRunner\ProcessRunner; use Deployer\Component\Ssh\Client; +use Deployer\Exception\ConfigurationException; use Gaambo\DeployerWordpress\NPM; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Console\Output\OutputInterface; @@ -193,4 +194,156 @@ public static function verbosityProvider(): array 'debug' => [false, false, true, '-ddd'], ]; } + + public function testRunCommandWithComplexArguments(): void + { + $path = '/var/www/html'; + $action = 'install'; + $arguments = '--save-dev "package@1.0.0" --save-exact --no-audit --legacy-peer-deps'; + $expectedCommand = "cd $path && npm $action $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('NPM output'); + + $result = NPM::runCommand($path, $action, $arguments); + $this->assertEquals('NPM output', $result); + } + + public function testRunCommandWithSpecialCharactersInPath(): void + { + $path = '/var/www/html with spaces'; + $action = 'install'; + $arguments = '--save-dev'; + $expectedCommand = "cd $path && npm $action $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('NPM output'); + + $result = NPM::runCommand($path, $action, $arguments); + $this->assertEquals('NPM output', $result); + } + + public function testRunCommandWithVeryLongPath(): void + { + // Create a path that's 255 characters long (common filesystem limit) + $path = str_repeat('a', 200) . '/path/to/project'; + $action = 'install'; + $arguments = '--save-dev'; + $expectedCommand = "cd $path && npm $action $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('NPM output'); + + $result = NPM::runCommand($path, $action, $arguments); + $this->assertEquals('NPM output', $result); + } + + public function testRunCommandWithComplexScriptName(): void + { + $path = '/var/www/html'; + $script = 'build:production:minify'; + $arguments = '--env=prod'; + $expectedCommand = "cd $path && npm run-script $script $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('NPM output'); + + $result = NPM::runScript($path, $script, $arguments); + $this->assertEquals('NPM output', $result); + } + + public function testRunCommandWithCustomNpmBinary(): void + { + $path = '/var/www/html'; + $action = 'install'; + $arguments = '--save-dev'; + $expectedCommand = "cd $path && /usr/local/bin/npm $action $arguments"; + + // Set custom npm binary path + $this->deployer->config->set('bin/npm', '/usr/local/bin/npm'); + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('NPM output'); + + $result = NPM::runCommand($path, $action, $arguments); + $this->assertEquals('NPM output', $result); + } + + public function testRunInstallWithCustomPreviousReleasePath(): void + { + $path = '/var/www/html'; + $customReleasePath = '/var/www/custom/releases/2'; + + // Set custom previous_release path + $this->deployer->config->set('previous_release', $customReleasePath); + + $this->processRunnerMock + ->expects($this->exactly(3)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($path, $customReleasePath) { + static $callNumber = 0; + $callNumber++; + + switch ($callNumber) { + case 1: + $this->assertStringContainsString("if [ -d $customReleasePath/node_modules ]; then echo +", $command); + if (preg_match('/echo \+([a-z]+);/', $command, $matches)) { + return '+' . $matches[1]; + } + throw new \RuntimeException('Could not extract random value from command'); + case 2: + $this->assertEquals("cp -R $customReleasePath/node_modules $path", $command); + return 'Copy output'; + case 3: + $this->assertEquals("cd $path && npm install", $command); + return 'NPM output'; + } + }); + + $result = NPM::runInstall($path); + $this->assertEquals('NPM output', $result); + } + + public function testRunCommandWithInvalidNpmBinary(): void + { + $path = '/var/www/html'; + $action = 'install'; + $arguments = '--save-dev'; + $expectedCommand = "cd $path && npm $action $arguments"; + + // Set invalid npm binary (should fall back to default) + $this->deployer->config->set('bin/npm', null); + + $this->expectException(ConfigurationException::class); + $this->expectExceptionMessage('Config option "bin/npm" does not exist'); + $result = NPM::runCommand($path, $action, $arguments); + } + + public function testRunInstallWithInvalidPreviousReleasePath(): void + { + $path = '/var/www/html'; + + // Set invalid previous_release path (should skip copying) + $this->deployer->config->set('previous_release', null); + + $this->expectException(ConfigurationException::class); + $this->expectExceptionMessage('Config option "previous_release" does not exist'); + + $result = NPM::runInstall($path); + } } \ No newline at end of file diff --git a/tests/Integration/RsyncIntegrationTest.php b/tests/Integration/RsyncIntegrationTest.php index 61da3b1..8a70952 100644 --- a/tests/Integration/RsyncIntegrationTest.php +++ b/tests/Integration/RsyncIntegrationTest.php @@ -178,4 +178,220 @@ public function testBuildOptionsArrayFiltersEmptyStrings(): void $this->assertNotContains('--include=', $result, "Empty include found in result"); $this->assertNotContains('--filter=', $result, "Empty filter found in result"); } + + public function testBuildOptionsArrayWithComplexPaths(): void + { + $config = [ + 'exclude' => [ + 'path/with spaces/file.txt', + 'path/with/special@chars', + 'path/with/backslash\\file.txt', + 'path/with/dollar$file.txt', + 'path/with/asterisk*file.txt', + 'path/with/question?file.txt' + ], + 'include' => [ + 'path/with/quotes"file.txt', + 'path/with/single\'quote.txt', + 'path/with/backtick`file.txt' + ] + ]; + + $result = Rsync::buildOptionsArray($config); + + // Test spaces in paths + $this->assertContains('--exclude=path/with spaces/file.txt', $result); + + // Test special characters + $this->assertContains('--exclude=path/with/special@chars', $result); + $this->assertContains('--exclude=path/with/backslash\\file.txt', $result); + $this->assertContains('--exclude=path/with/dollar$file.txt', $result); + $this->assertContains('--exclude=path/with/asterisk*file.txt', $result); + $this->assertContains('--exclude=path/with/question?file.txt', $result); + + // Test quotes and backticks + $this->assertContains('--include=path/with/quotes"file.txt', $result); + $this->assertContains('--include=path/with/single\'quote.txt', $result); + $this->assertContains('--include=path/with/backtick`file.txt', $result); + } + + public function testBuildOptionsArrayWithRelativePaths(): void + { + $config = [ + 'exclude' => [ + './local/path', + '../parent/path', + '../../root/path', + '././nested/./path', + '.././mixed/../path' + ], + 'include' => [ + '~/home/path', + '~user/home/path' + ] + ]; + + $result = Rsync::buildOptionsArray($config); + + // Test relative paths + $this->assertContains('--exclude=./local/path', $result); + $this->assertContains('--exclude=../parent/path', $result); + $this->assertContains('--exclude=../../root/path', $result); + + // Test mixed relative paths + $this->assertContains('--exclude=././nested/./path', $result); + $this->assertContains('--exclude=.././mixed/../path', $result); + + // Test home directory paths + $this->assertContains('--include=~/home/path', $result); + $this->assertContains('--include=~user/home/path', $result); + } + + public function testBuildOptionsArrayWithEmptyAndWhitespaceValues(): void + { + $config = [ + 'options' => ['', ' ', ' ', "\t", "\n", 'valid-option'], + 'exclude' => ['', ' ', ' ', "\t", "\n", 'valid-exclude'], + 'include' => ['', ' ', ' ', "\t", "\n", 'valid-include'], + 'filter' => ['', ' ', ' ', "\t", "\n", 'valid-filter'] + ]; + + $result = Rsync::buildOptionsArray($config); + + // Check that non-empty values are present + $this->assertContains('--valid-option', $result); + $this->assertContains('--exclude=valid-exclude', $result); + $this->assertContains('--include=valid-include', $result); + $this->assertContains('--filter=valid-filter', $result); + + // Check that no empty values are present + $this->assertNotContains('', $result, "Empty string found in result"); + $this->assertNotContains('--exclude=', $result, "Empty exclude found in result"); + $this->assertNotContains('--include=', $result, "Empty include found in result"); + $this->assertNotContains('--filter=', $result, "Empty filter found in result"); + + // Check that defaults are not present (user values should override) + $this->assertNotContains('--delete-after', $result); + $this->assertNotContains('--exclude=.git', $result); + $this->assertNotContains('--exclude=deploy.php', $result); + } + + public function testBuildOptionsArrayWithVeryLongPaths(): void + { + // Create a path that's 255 characters long (common filesystem limit) + $longPath = str_repeat('a', 200) . '/path/to/file.txt'; + + $config = [ + 'exclude' => [$longPath], + 'include' => [$longPath], + 'filter' => ['+ ' . $longPath] + ]; + + $result = Rsync::buildOptionsArray($config); + + $this->assertContains('--exclude=' . $longPath, $result); + $this->assertContains('--include=' . $longPath, $result); + $this->assertContains('--filter=+ ' . $longPath, $result); + } + + public function testBuildOptionsArrayWithUnicodeCharacters(): void + { + $config = [ + 'exclude' => [ + 'path/with/unicode/测试.txt', + 'path/with/unicode/über.txt', + 'path/with/unicode/🎉.txt' + ], + 'include' => [ + 'path/with/unicode/日本語.txt', + 'path/with/unicode/русский.txt' + ] + ]; + + $result = Rsync::buildOptionsArray($config); + + $this->assertContains('--exclude=path/with/unicode/测试.txt', $result); + $this->assertContains('--exclude=path/with/unicode/über.txt', $result); + $this->assertContains('--exclude=path/with/unicode/🎉.txt', $result); + $this->assertContains('--include=path/with/unicode/日本語.txt', $result); + $this->assertContains('--include=path/with/unicode/русский.txt', $result); + } + + public function testBuildOptionsArrayWithInvalidConfigurationTypes(): void + { + $config = [ + 'exclude' => 'not-an-array', + 'include' => 123, + 'filter' => true, + 'options' => null, + 'exclude-file' => ['should-be-string'], + 'include-file' => 456, + 'filter-file' => false, + 'filter-perdir' => ['should-be-string'] + ]; + + $result = Rsync::buildOptionsArray($config); + + $this->assertContains('--exclude=not-an-array', $result); + $this->assertNotContains('--exclude=deploy.php', $result); // Not default value + $this->assertNotContains('--delete-after', $result); // Not default value + $this->assertNotContains('--exclude-file=should-be-string', $result); + $this->assertNotContains('--filter=dir-merge should-be-string', $result); + } + + public function testBuildOptionsArrayWithEmptyConfigurationValues(): void + { + $config = [ + 'exclude' => [], + 'include' => [], + 'filter' => [], + 'options' => [] + ]; + + $result = Rsync::buildOptionsArray($config); + + // Empty arrays should replace defaults + $this->assertNotContains('--exclude=.git', $result); + $this->assertNotContains('--exclude=deploy.php', $result); + $this->assertNotContains('--delete-after', $result); + } + + public function testBuildOptionsArrayWithPartialConfiguration(): void + { + $config = [ + 'exclude' => ['custom.txt'], + 'options' => ['recursive'] + ]; + + $result = Rsync::buildOptionsArray($config); + + // Should use custom values over defaults + $this->assertContains('--exclude=custom.txt', $result); + $this->assertNotContains('--exclude=.git', $result); + $this->assertNotContains('--exclude=deploy.php', $result); + $this->assertContains('--recursive', $result); + $this->assertNotContains('--delete-after', $result); + } + + public function testBuildOptionsArrayWithOverriddenDefaults(): void + { + $config = [ + 'exclude' => ['custom.txt'], + 'options' => ['recursive'] + ]; + + // Override default config + $this->deployer->config->set('rsync', [ + 'exclude' => ['default.txt'], + 'options' => ['verbose'] + ]); + + $result = Rsync::buildOptionsArray($config); + + // Should use custom values over defaults + $this->assertContains('--exclude=custom.txt', $result); + $this->assertNotContains('--exclude=default.txt', $result); + $this->assertContains('--recursive', $result); + $this->assertNotContains('--verbose', $result); + } } diff --git a/tests/Integration/WpCliIntegrationTest.php b/tests/Integration/WpCliIntegrationTest.php index 0dfb0b4..0808c86 100644 --- a/tests/Integration/WpCliIntegrationTest.php +++ b/tests/Integration/WpCliIntegrationTest.php @@ -151,4 +151,199 @@ public function testInstallWithDefaultBinaryName(): void $result = WPCLI::install($installPath); $this->assertEquals("$installPath/wp-cli.phar", $result); } + + public function testRunCommandWithComplexArguments(): void + { + $path = '/var/www/html'; + $command = 'post create'; + $arguments = '--post_title="Complex Title with Spaces" --post_content="Content with \'quotes\' and \"double quotes\"" --post_status=draft'; + $expectedCommand = "cd $path && wp $command $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('WP-CLI output'); + + WPCLI::runCommand($command, $path, $arguments); + } + + public function testRunCommandWithSpecialCharactersInPath(): void + { + $path = '/var/www/html with spaces'; + $command = 'post list'; + $arguments = '--format=table'; + $expectedCommand = "cd $path && wp $command $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('WP-CLI output'); + + WPCLI::runCommand($command, $path, $arguments); + } + + public function testRunCommandWithVeryLongPath(): void + { + // Create a path that's 255 characters long (common filesystem limit) + $path = str_repeat('a', 200) . '/path/to/wordpress'; + $command = 'post list'; + $arguments = '--format=table'; + $expectedCommand = "cd $path && wp $command $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('WP-CLI output'); + + WPCLI::runCommand($command, $path, $arguments); + } + + public function testRunCommandWithComplexCommand(): void + { + $path = '/var/www/html'; + $command = 'post create --post_type=page --post_status=publish --post_title="Home Page" --post_content="Welcome to our site"'; + $arguments = '--meta_input=\'{"_wp_page_template":"page-home.php"}\''; + $expectedCommand = "cd $path && wp $command $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('WP-CLI output'); + + WPCLI::runCommand($command, $path, $arguments); + } + + public function testRunCommandLocallyWithComplexPath(): void + { + $path = '/var/www/html with spaces and special chars @#$%'; + $command = 'post list'; + $arguments = '--format=table'; + $expectedCommand = "cd $path && wp $command $arguments"; + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with( + $this->anything(), + $expectedCommand, + [] + ); + + WPCLI::runCommandLocally($command, $path, $arguments); + } + + public function testRunCommandWithCustomWpBinary(): void + { + $path = '/var/www/html'; + $command = 'post list'; + $arguments = '--format=table'; + $expectedCommand = "cd $path && /usr/local/bin/wp $command $arguments"; + + // Set custom wp binary path + $this->deployer->config->set('bin/wp', '/usr/local/bin/wp'); + $this->host->config()->set('bin/wp', '/usr/local/bin/wp'); + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('WP-CLI output'); + + WPCLI::runCommand($command, $path, $arguments); + } + + public function testRunCommandLocallyWithCustomWpBinary(): void + { + $path = '/var/www/html'; + $command = 'post list'; + $arguments = '--format=table'; + $expectedCommand = "cd $path && /usr/local/bin/wp $command $arguments"; + + // Set custom wp binary path for localhost + $this->deployer->config->set('localhost.bin/wp', '/usr/local/bin/wp'); + $this->host->config()->set('bin/wp', '/usr/local/bin/wp'); + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with( + $this->anything(), + $expectedCommand, + [] + ); + + WPCLI::runCommandLocally($command, $path, $arguments); + } + + public function testRunCommandWithInvalidWpBinary(): void + { + $path = '/var/www/html'; + $command = 'post list'; + $arguments = '--format=table'; + $expectedCommand = "cd $path && wp $command $arguments"; + + // Set invalid wp binary (should fall back to default) + $this->deployer->config->set('bin/wp', null); + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with($this->host, $expectedCommand) + ->willReturn('WP-CLI output'); + + WPCLI::runCommand($command, $path, $arguments); + } + + public function testRunCommandLocallyWithInvalidWpBinary(): void + { + $path = '/var/www/html'; + $command = 'post list'; + $arguments = '--format=table'; + $expectedCommand = "cd $path && wp $command $arguments"; + + // Set invalid wp binary for localhost (should fall back to default) + $this->deployer->config->set('localhost.bin/wp', null); + + $this->processRunnerMock + ->expects($this->once()) + ->method('run') + ->with( + $this->anything(), + $expectedCommand, + [] + ); + + WPCLI::runCommandLocally($command, $path, $arguments); + } + + public function testInstallWithCustomBinaryPath(): void + { + $installPath = '/usr/local/bin'; + $binaryName = 'wp'; + $expectedCommands = [ + "sudo mkdir -p $installPath", + "sudo cd $installPath && curl -sS https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o wp-cli.phar", + "sudo mv $installPath/wp-cli.phar $installPath/$binaryName" + ]; + + // Set custom binary path + $this->deployer->config->set('bin/wp', "$installPath/$binaryName"); + + $this->processRunnerMock + ->expects($this->exactly(3)) + ->method('run') + ->willReturnCallback(function ($host, $command) use ($expectedCommands) { + static $index = 0; + $this->assertEquals($expectedCommands[$index], $command); + $index++; + return 'Installation output'; + }); + + $result = WPCLI::install($installPath, $binaryName, true); + $this->assertEquals("$installPath/$binaryName", $result); + } } From 14580efe5238214a5156327b2724abc9d5a8bc76 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Mon, 24 Mar 2025 17:07:09 +0100 Subject: [PATCH 20/36] add task registration tests --- .../Tasks/DatabaseTasksRegistrationTest.php | 30 +++++++++++++ .../Tasks/FilesTasksRegistrationTest.php | 44 +++++++++++++++++++ .../Tasks/LanguagesTasksRegistrationTest.php | 28 ++++++++++++ .../Tasks/MuPluginsTasksRegistrationTest.php | 31 +++++++++++++ .../Tasks/PackagesTasksRegistrationTest.php | 35 +++++++++++++++ .../Tasks/PluginsTasksRegistrationTest.php | 28 ++++++++++++ .../Tasks/TaskRegistrationTestCase.php | 41 +++++++++++++++++ .../Tasks/ThemesTasksRegistrationTest.php | 35 +++++++++++++++ .../Tasks/UploadsTasksRegistrationTest.php | 28 ++++++++++++ .../Tasks/WpTasksRegistrationTest.php | 31 +++++++++++++ 10 files changed, 331 insertions(+) create mode 100644 tests/Integration/Tasks/DatabaseTasksRegistrationTest.php create mode 100644 tests/Integration/Tasks/FilesTasksRegistrationTest.php create mode 100644 tests/Integration/Tasks/LanguagesTasksRegistrationTest.php create mode 100644 tests/Integration/Tasks/MuPluginsTasksRegistrationTest.php create mode 100644 tests/Integration/Tasks/PackagesTasksRegistrationTest.php create mode 100644 tests/Integration/Tasks/PluginsTasksRegistrationTest.php create mode 100644 tests/Integration/Tasks/TaskRegistrationTestCase.php create mode 100644 tests/Integration/Tasks/ThemesTasksRegistrationTest.php create mode 100644 tests/Integration/Tasks/UploadsTasksRegistrationTest.php create mode 100644 tests/Integration/Tasks/WpTasksRegistrationTest.php diff --git a/tests/Integration/Tasks/DatabaseTasksRegistrationTest.php b/tests/Integration/Tasks/DatabaseTasksRegistrationTest.php new file mode 100644 index 0000000..40e610d --- /dev/null +++ b/tests/Integration/Tasks/DatabaseTasksRegistrationTest.php @@ -0,0 +1,30 @@ +assertTaskExists('db:remote:backup'); + $this->assertTaskExists('db:local:backup'); + $this->assertTaskExists('db:remote:import'); + $this->assertTaskExists('db:local:import'); + $this->assertTaskExists('db:push'); + $this->assertTaskExists('db:pull'); + } + + public function testTaskDependencies(): void + { + // Verify task dependencies + $this->assertTaskDependencies('db:push', ['db:local:backup', 'db:remote:import']); + $this->assertTaskDependencies('db:pull', ['db:remote:backup', 'db:local:import']); + } +} diff --git a/tests/Integration/Tasks/FilesTasksRegistrationTest.php b/tests/Integration/Tasks/FilesTasksRegistrationTest.php new file mode 100644 index 0000000..2c0be53 --- /dev/null +++ b/tests/Integration/Tasks/FilesTasksRegistrationTest.php @@ -0,0 +1,44 @@ +assertTaskExists('files:push'); + $this->assertTaskExists('files:pull'); + $this->assertTaskExists('files:sync'); + $this->assertTaskExists('files:backup:remote'); + $this->assertTaskExists('files:backup:local'); + } + + public function testTaskDependencies(): void + { + // Verify task dependencies + $this->assertTaskDependencies('files:push', [ + 'wp:push', + 'uploads:push', + 'plugins:push', + 'mu-plugins:push', + 'themes:push', + 'packages:push' + ]); + $this->assertTaskDependencies('files:pull', [ + 'wp:pull', + 'uploads:pull', + 'plugins:pull', + 'mu-plugins:pull', + 'themes:pull', + 'packages:pull' + ]); + $this->assertTaskDependencies('files:sync', ['files:push', 'files:pull']); + } +} diff --git a/tests/Integration/Tasks/LanguagesTasksRegistrationTest.php b/tests/Integration/Tasks/LanguagesTasksRegistrationTest.php new file mode 100644 index 0000000..ca39d6c --- /dev/null +++ b/tests/Integration/Tasks/LanguagesTasksRegistrationTest.php @@ -0,0 +1,28 @@ +assertTaskExists('languages:push'); + $this->assertTaskExists('languages:pull'); + $this->assertTaskExists('languages:sync'); + $this->assertTaskExists('languages:backup:remote'); + $this->assertTaskExists('languages:backup:local'); + } + + public function testTaskDependencies(): void + { + // Verify task dependencies + $this->assertTaskDependencies('languages:sync', ['languages:push', 'languages:pull']); + } +} diff --git a/tests/Integration/Tasks/MuPluginsTasksRegistrationTest.php b/tests/Integration/Tasks/MuPluginsTasksRegistrationTest.php new file mode 100644 index 0000000..0d727f0 --- /dev/null +++ b/tests/Integration/Tasks/MuPluginsTasksRegistrationTest.php @@ -0,0 +1,31 @@ +assertTaskExists('mu-plugin:vendors'); + $this->assertTaskExists('mu-plugin'); + $this->assertTaskExists('mu-plugins:push'); + $this->assertTaskExists('mu-plugins:pull'); + $this->assertTaskExists('mu-plugins:sync'); + $this->assertTaskExists('mu-plugins:backup:remote'); + $this->assertTaskExists('mu-plugins:backup:local'); + } + + public function testTaskDependencies(): void + { + // Verify task dependencies + $this->assertTaskDependencies('mu-plugin', ['mu-plugin:vendors']); + $this->assertTaskDependencies('mu-plugins:sync', ['mu-plugins:push', 'mu-plugins:pull']); + } +} diff --git a/tests/Integration/Tasks/PackagesTasksRegistrationTest.php b/tests/Integration/Tasks/PackagesTasksRegistrationTest.php new file mode 100644 index 0000000..1f0e8bd --- /dev/null +++ b/tests/Integration/Tasks/PackagesTasksRegistrationTest.php @@ -0,0 +1,35 @@ +assertTaskExists('packages:assets:vendors'); + $this->assertTaskExists('packages:assets:build'); + $this->assertTaskExists('packages:assets'); + $this->assertTaskExists('packages:vendors'); + $this->assertTaskExists('packages'); + $this->assertTaskExists('packages:push'); + $this->assertTaskExists('packages:pull'); + $this->assertTaskExists('packages:sync'); + $this->assertTaskExists('packages:backup:remote'); + $this->assertTaskExists('packages:backup:local'); + } + + public function testTaskDependencies(): void + { + // Verify task dependencies + $this->assertTaskDependencies('packages:assets', ['packages:assets:vendors', 'packages:assets:build']); + $this->assertTaskDependencies('packages', ['packages:assets', 'packages:vendors']); + $this->assertTaskDependencies('packages:sync', ['packages:push', 'packages:pull']); + } +} diff --git a/tests/Integration/Tasks/PluginsTasksRegistrationTest.php b/tests/Integration/Tasks/PluginsTasksRegistrationTest.php new file mode 100644 index 0000000..e335339 --- /dev/null +++ b/tests/Integration/Tasks/PluginsTasksRegistrationTest.php @@ -0,0 +1,28 @@ +assertTaskExists('plugins:push'); + $this->assertTaskExists('plugins:pull'); + $this->assertTaskExists('plugins:sync'); + $this->assertTaskExists('plugins:backup:remote'); + $this->assertTaskExists('plugins:backup:local'); + } + + public function testTaskDependencies(): void + { + // Verify task dependencies + $this->assertTaskDependencies('plugins:sync', ['plugins:push', 'plugins:pull']); + } +} diff --git a/tests/Integration/Tasks/TaskRegistrationTestCase.php b/tests/Integration/Tasks/TaskRegistrationTestCase.php new file mode 100644 index 0000000..3ce50de --- /dev/null +++ b/tests/Integration/Tasks/TaskRegistrationTestCase.php @@ -0,0 +1,41 @@ +assertTrue( + $this->deployer->tasks->has($taskName), + "Task '$taskName' should be registered" + ); + } + + /** + * @param string $taskName + * @param string[] $expectedDependencies + * @return void + */ + protected function assertTaskDependencies(string $taskName, array $expectedDependencies): void + { + $task = $this->deployer->tasks->get($taskName); + $this->assertInstanceOf(GroupTask::class, $task); + $this->assertEquals($expectedDependencies, $task->getGroup()); + } + + abstract protected function testTasksAreRegistered(): void; + abstract protected function testTaskDependencies(): void; +} diff --git a/tests/Integration/Tasks/ThemesTasksRegistrationTest.php b/tests/Integration/Tasks/ThemesTasksRegistrationTest.php new file mode 100644 index 0000000..1ce5d98 --- /dev/null +++ b/tests/Integration/Tasks/ThemesTasksRegistrationTest.php @@ -0,0 +1,35 @@ +assertTaskExists('theme:assets:vendors'); + $this->assertTaskExists('theme:assets:build'); + $this->assertTaskExists('theme:assets'); + $this->assertTaskExists('theme:vendors'); + $this->assertTaskExists('theme'); + $this->assertTaskExists('themes:push'); + $this->assertTaskExists('themes:pull'); + $this->assertTaskExists('themes:sync'); + $this->assertTaskExists('themes:backup:remote'); + $this->assertTaskExists('themes:backup:local'); + } + + public function testTaskDependencies(): void + { + // Verify task dependencies + $this->assertTaskDependencies('theme:assets', ['theme:assets:vendors', 'theme:assets:build']); + $this->assertTaskDependencies('theme', ['theme:assets', 'theme:vendors']); + $this->assertTaskDependencies('themes:sync', ['themes:push', 'themes:pull']); + } +} diff --git a/tests/Integration/Tasks/UploadsTasksRegistrationTest.php b/tests/Integration/Tasks/UploadsTasksRegistrationTest.php new file mode 100644 index 0000000..d7202aa --- /dev/null +++ b/tests/Integration/Tasks/UploadsTasksRegistrationTest.php @@ -0,0 +1,28 @@ +assertTaskExists('uploads:push'); + $this->assertTaskExists('uploads:pull'); + $this->assertTaskExists('uploads:sync'); + $this->assertTaskExists('uploads:backup:remote'); + $this->assertTaskExists('uploads:backup:local'); + } + + public function testTaskDependencies(): void + { + // Verify task dependencies + $this->assertTaskDependencies('uploads:sync', ['uploads:push', 'uploads:pull']); + } +} diff --git a/tests/Integration/Tasks/WpTasksRegistrationTest.php b/tests/Integration/Tasks/WpTasksRegistrationTest.php new file mode 100644 index 0000000..84322c6 --- /dev/null +++ b/tests/Integration/Tasks/WpTasksRegistrationTest.php @@ -0,0 +1,31 @@ +assertTaskExists('wp:download-core'); + $this->assertTaskExists('wp:push'); + $this->assertTaskExists('wp:pull'); + $this->assertTaskExists('wp:info'); + $this->assertTaskExists('wp:install-wpcli'); + } + + /** + * @doesNotPerformAssertions + * @return void + */ + public function testTaskDependencies(): void + { + // No grouped tasks in wp.php + } +} From c52154243e45d7b4bdd2ca5759f77e00ef2d07e6 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Tue, 25 Mar 2025 16:41:46 +0100 Subject: [PATCH 21/36] start with functional task tests - add database tests rename dump_paths to use dbdump/ prefix --- .github/workflows/tests.yml | 2 +- composer.json | 18 +- composer.lock | 73 ++- examples/bedrock/deploy.php | 2 +- examples/bedrock/deploy.yml | 2 +- examples/simple/deploy.php | 2 +- examples/simple/deploy.yml | 2 +- phpunit.xml | 4 + recipes/bedrock.php | 2 +- recipes/common.php | 29 +- recipes/simple.php | 2 +- tasks/database.php | 54 +- tests/Fixtures/database/dump.sql | 18 + tests/Fixtures/recipes/common.php | 3 + tests/Functional/FunctionalTestCase.php | 224 +++++++ .../Tasks/DatabaseTasksFunctionalTest.php | 616 ++++++++++++++++++ 16 files changed, 1006 insertions(+), 47 deletions(-) create mode 100644 tests/Fixtures/database/dump.sql create mode 100644 tests/Fixtures/recipes/common.php create mode 100644 tests/Functional/FunctionalTestCase.php create mode 100644 tests/Functional/Tasks/DatabaseTasksFunctionalTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 86a1ad3..41cc8d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,4 +46,4 @@ jobs: # Run PHPUnit tests - name: Run PHPUnit tests - run: composer run-script test \ No newline at end of file + run: composer run-script test \ No newline at end of file diff --git a/composer.json b/composer.json index 940dead..0740f14 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^10.5", "squizlabs/php_codesniffer": "^3.11", - "symfony/console": "^6.4" + "symfony/console": "^6.4", + "symfony/process": "^6.4" }, "require": { "php": "^8.1", @@ -36,8 +37,19 @@ "phpcs:fix": "vendor/bin/phpcbf", "phpstan": "vendor/bin/phpstan analyse --memory-limit 1G", "lint": "find . -type f -name '*.php' -not -path './vendor/*' -exec php -l {} \\;", - "test": "vendor/bin/phpunit", - "test:coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text --filter 'Gaambo\\\\DeployerWordpress\\\\'", + "phpunit": "DO_NOT_TRACK=true vendor/bin/phpunit", + "tests:unit": "DO_NOT_TRACK=true vendor/bin/phpunit --testsuite Unit", + "tests:unit:coverage": "DO_NOT_TRACK=true XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite Unit --coverage-text", + "tests:integration": "DO_NOT_TRACK=true vendor/bin/phpunit --testsuite Integration", + "tests:integration:coverage": "DO_NOT_TRACK=true XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite Integration --coverage-text", + "tests:functional": "DO_NOT_TRACK=true vendor/bin/phpunit --testsuite Functional", + "tests:functional:coverage": "DO_NOT_TRACK=true XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite Functional --coverage-text", + "tests": [ + "@test:unit", + "@test:integration", + "@test:functional" + ], + "tests:coverage": "DO_NOT_TRACK=true XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text", "precommit": [ "@lint", "@phpcs", diff --git a/composer.lock b/composer.lock index fe60991..4a4f822 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1df277ea9805fbaebf8f6b3bce85b59e", + "content-hash": "36f9332495bad0743f8cd95063bef290", "packages": [ { "name": "deployer/deployer", @@ -300,16 +300,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.10", + "version": "2.1.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "051a3b6b9b80df4ba3a7f801a8b53ad7d8f1c15f" + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/051a3b6b9b80df4ba3a7f801a8b53ad7d8f1c15f", - "reference": "051a3b6b9b80df4ba3a7f801a8b53ad7d8f1c15f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30", "shasum": "" }, "require": { @@ -354,7 +354,7 @@ "type": "github" } ], - "time": "2025-03-23T14:57:55+00:00" + "time": "2025-03-24T13:45:00+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2310,6 +2310,67 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/process", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", + "reference": "7a1c12e87b08ec9c97abdd188c9b3f5a40e37fc3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-04T13:35:48+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.5.1", diff --git a/examples/bedrock/deploy.php b/examples/bedrock/deploy.php index 320572b..62150b1 100644 --- a/examples/bedrock/deploy.php +++ b/examples/bedrock/deploy.php @@ -33,7 +33,7 @@ ->set('themes/dir', 'web/app/themes') ->set('plugins/dir', 'web/app/plugins') ->set('wp/dir', 'web/wp') - ->set('dump_path', __DIR__ . '/data/db_dumps') + ->set('dbdump/path', __DIR__ . '/data/db_dumps') ->set('backup_path', __DIR__ . '/data/backups'); set('packages', [ diff --git a/examples/bedrock/deploy.yml b/examples/bedrock/deploy.yml index 4852590..7ab42e4 100644 --- a/examples/bedrock/deploy.yml +++ b/examples/bedrock/deploy.yml @@ -14,5 +14,5 @@ hosts: themes/dir: web/app/themes plugins/dir: web/app/plugins wp/dir: web/wp - dump_path: ~/data/dumps + dbdump/path: ~/data/dumps backup_path: ~/data/backups diff --git a/examples/simple/deploy.php b/examples/simple/deploy.php index 674a32c..26b60bc 100644 --- a/examples/simple/deploy.php +++ b/examples/simple/deploy.php @@ -26,7 +26,7 @@ ->set('current_path', function () { return Localhost::getConfig('release_path'); }) - ->set('dump_path', __DIR__ . '/data/db_dumps') + ->set('dbdump/path', __DIR__ . '/data/db_dumps') ->set('backup_path', __DIR__ . '/data/backups'); set('packages', [ diff --git a/examples/simple/deploy.yml b/examples/simple/deploy.yml index e31d781..0602b08 100644 --- a/examples/simple/deploy.yml +++ b/examples/simple/deploy.yml @@ -10,5 +10,5 @@ hosts: public_url: https://test.dev deploy_path: "~" release_path: "{{deploy_path}}/public_html" # fixed directory, no symlinks - dump_path: ~/data/dumps + dbdump/path: ~/data/dumps backup_path: ~/data/backups diff --git a/phpunit.xml b/phpunit.xml index 7f8c481..cc27992 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,10 +7,14 @@ tests/Integration + + tests/Functional + src + tasks tests diff --git a/recipes/bedrock.php b/recipes/bedrock.php index 5b535b0..fbf0fd8 100644 --- a/recipes/bedrock.php +++ b/recipes/bedrock.php @@ -20,7 +20,7 @@ use function Deployer\test; use function Deployer\upload; -require_once __DIR__ . '/common.php'; +require __DIR__ . '/common.php'; add('recipes', ['bedrock-wp']); diff --git a/recipes/common.php b/recipes/common.php index f471fa3..c11179c 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -32,18 +32,27 @@ // Deployer binary sets the include path, so this should work. -$deployerPath = 'vendor/deployer/deployer/'; -require_once $deployerPath . 'recipe/common.php'; +$commonRecipePaths = [ + __DIR__ . '/../vendor/deployer/deployer/recipe/common.php', // Development/testing + __DIR__ . '/../deployer/deployer/recipe/common.php' // Installed via composer +]; + +foreach ($commonRecipePaths as $recipePath) { + if (file_exists($recipePath)) { + require $recipePath; + break; + } +} // Include task definitions -require_once __DIR__ . '/../tasks/database.php'; -require_once __DIR__ . '/../tasks/files.php'; -require_once __DIR__ . '/../tasks/mu-plugins.php'; -require_once __DIR__ . '/../tasks/packages.php'; -require_once __DIR__ . '/../tasks/plugins.php'; -require_once __DIR__ . '/../tasks/themes.php'; -require_once __DIR__ . '/../tasks/uploads.php'; -require_once __DIR__ . '/../tasks/wp.php'; +require __DIR__ . '/../tasks/database.php'; +require __DIR__ . '/../tasks/files.php'; +require __DIR__ . '/../tasks/mu-plugins.php'; +require __DIR__ . '/../tasks/packages.php'; +require __DIR__ . '/../tasks/plugins.php'; +require __DIR__ . '/../tasks/themes.php'; +require __DIR__ . '/../tasks/uploads.php'; +require __DIR__ . '/../tasks/wp.php'; // BINARIES set('bin/npm', function () { diff --git a/recipes/simple.php b/recipes/simple.php index cc4e7a4..0f98611 100644 --- a/recipes/simple.php +++ b/recipes/simple.php @@ -15,7 +15,7 @@ use function Deployer\task; use function Deployer\test; -require_once __DIR__ . '/common.php'; +require __DIR__ . '/common.php'; add('recipes', ['simple-wp']); diff --git a/tasks/database.php b/tasks/database.php index d10a060..9f47511 100644 --- a/tasks/database.php +++ b/tasks/database.php @@ -18,58 +18,61 @@ use function Deployer\download; use function Deployer\get; +use function Deployer\has; use function Deployer\run; use function Deployer\runLocally; use function Deployer\set; use function Deployer\task; +use function Deployer\test; +use function Deployer\testLocally; use function Deployer\upload; /** * Create backup of remote database and download locally * * Configuration: - * - dump_path: Directory to store database dumps (required on both local and remote) + * - dbdump/path: Directory to store database dumps (required on both local and remote) * - bin/wp: WP-CLI binary/command to use (automatically configured) * * Example: * dep db:remote:backup prod */ task('db:remote:backup', function () { - $localDumpPath = Localhost::getConfig('dump_path'); + $localDumpPath = Localhost::getConfig('dbdump/path'); + $remoteDumpPath = get('dbdump/path'); $now = date('Y-m-d_H-i', time()); - set('dump_file', "db_backup-$now.sql"); - set('dump_filepath', get('dump_path') . '/' . get('dump_file')); + set('dbdump/file', "db_backup-$now.sql"); - run('mkdir -p ' . get('dump_path')); - WPCLI::runCommand("db export {{dump_filepath}} --add-drop-table", "{{release_or_current_path}}"); + run('mkdir -p ' . get('dbdump/path')); + WPCLI::runCommand("db export $remoteDumpPath/{{dbdump/file}} --add-drop-table", "{{release_or_current_path}}"); runLocally("mkdir -p $localDumpPath"); - download('{{dump_filepath}}', "$localDumpPath/{{dump_file}}"); + download("$remoteDumpPath/{{dbdump/file}}", "$localDumpPath/{{dbdump/file}}"); })->desc('Create backup of remote database and download locally'); /** * Create backup of local database and upload to remote * * Configuration: - * - dump_path: Directory to store database dumps (required on both local and remote) + * - dbdump/path: Directory to store database dumps (required on both local and remote) * - bin/wp: WP-CLI binary/command to use (automatically configured) * * Example: * dep db:local:backup prod */ task('db:local:backup', function () { - $localDumpPath = Localhost::getConfig('dump_path'); + $localDumpPath = Localhost::getConfig('dbdump/path'); + $remoteDumpPath = get('dbdump/path'); $now = date('Y-m-d_H-i', time()); - set('dump_file', "db_backup-$now.sql"); - set('dump_filepath', '{{dump_path}}/{{dump_file}}'); + set('dbdump/file', "db_backup-$now.sql"); runLocally("mkdir -p $localDumpPath"); - WPCLI::runCommandLocally("db export $localDumpPath/{{dump_file}} --add-drop-table"); + WPCLI::runCommandLocally("db export $localDumpPath/{{dbdump/file}} --add-drop-table"); - run('mkdir -p {{dump_path}}'); + run('mkdir -p {{dbdump/path}}'); upload( - "$localDumpPath/{{dump_file}}", - '{{dump_filepath}}' + "$localDumpPath/{{dbdump/file}}", + "$remoteDumpPath/{{dbdump/file}}" ); })->desc('Create backup of local database and upload to remote'); @@ -85,8 +88,13 @@ * dep db:remote:import prod */ task('db:remote:import', function () { + // Check if dump file exists + if (!has('dbdump/file') || !test('[ -f {{dbdump/path}}/{{dbdump/file}} ]')) { + throw new \RuntimeException('Database dump file not found at {{dbdump/path}}/{{dbdump/file}}'); + } + $localUrl = Localhost::getConfig('public_url'); - WPCLI::runCommand("db import {{dump_filepath}}", "{{release_or_current_path}}"); + WPCLI::runCommand("db import {{dbdump/path}}/{{dbdump/file}}", "{{release_or_current_path}}"); WPCLI::runCommand("search-replace $localUrl {{public_url}}", "{{release_or_current_path}}"); // If the local uploads directory is different from the remote one @@ -96,7 +104,7 @@ WPCLI::runCommand("search-replace $localUploadsDir {{uploads/dir}}", "{{release_or_current_path}}"); } - run('rm -f {{dump_filepath}}'); + run('rm -f {{dbdump/path}}/{{dbdump/file}}'); })->desc('Import database backup on remote host'); /** @@ -106,15 +114,19 @@ * - bin/wp: WP-CLI binary/command to use (automatically configured) * - public_url: Site URL for both local and remote (required for URL replacement) * - uploads/dir: Upload directory path (for path replacement if different between environments) - * - dump_path: Directory containing database dumps + * - dbdump/path: Directory containing database dumps * * Example: * dep db:local:import prod */ task('db:local:import', function () { + // Check if dump file exists + $localDumpPath = Localhost::getConfig('dbdump/path'); + if (!has('dbdump/file') || !testLocally("[ -f $localDumpPath/{{dbdump/file}} ]")) { + throw new \RuntimeException("Database dump file not found at $localDumpPath/{{dbdump/file}}"); + } $localUrl = Localhost::getConfig('public_url'); - $localDumpPath = Localhost::getConfig('dump_path'); - WPCLI::runCommandLocally("db import $localDumpPath/{{dump_file}}"); + WPCLI::runCommandLocally("db import $localDumpPath/{{dbdump/file}}"); WPCLI::runCommandLocally("search-replace {{public_url}} $localUrl"); // If the local uploads directory is different from the remote one @@ -124,7 +136,7 @@ WPCLI::runCommandLocally("search-replace {{uploads/dir}} $localUploadsDir"); } - runLocally("rm -f $localDumpPath/{{dump_file}}"); + runLocally("rm -f $localDumpPath/{{dbdump/file}}"); })->desc('Import database backup on local host'); /** diff --git a/tests/Fixtures/database/dump.sql b/tests/Fixtures/database/dump.sql new file mode 100644 index 0000000..d4e4229 --- /dev/null +++ b/tests/Fixtures/database/dump.sql @@ -0,0 +1,18 @@ +-- WordPress database dump for testing +-- This is a mock SQL file to test database backup functionality + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; + +CREATE TABLE `wp_posts` +( + `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `post_title` text NOT NULL, + `post_content` longtext NOT NULL, + PRIMARY KEY (`ID`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + +INSERT INTO `wp_posts` (`post_title`, `post_content`) +VALUES ('Test Post 1', 'This is a test post content'), + ('Test Post 2', 'Another test post content'); diff --git a/tests/Fixtures/recipes/common.php b/tests/Fixtures/recipes/common.php new file mode 100644 index 0000000..ec89b12 --- /dev/null +++ b/tests/Fixtures/recipes/common.php @@ -0,0 +1,3 @@ +createTestDirectories(); + + // 2. Initialize Deployer and Application + $console = new Application(); + $console->setAutoExit(false); + $this->tester = new ApplicationTester($console); + $this->deployer = new Deployer($console); + $this->deployer->importer->import(static::RECIPE_PATH); + $this->deployer->init(); + + // 3. Configure hosts + $this->setUpHosts(); + + // 4. Set up mocked services + $this->setUpMockedServices(); + } + + /** + * Creates a unique test directory structure for this test run + */ + private function createTestDirectories(): void + { + $this->testDir = __TEMP_DIR__ . '/' . uniqid(); + $this->localDir = $this->testDir . '/local'; + $this->remoteDir = $this->testDir . '/remote'; + + mkdir($this->localDir, 0755, true); + mkdir($this->localDir . '/current', 0755, true); + mkdir($this->remoteDir, 0755, true); + mkdir($this->remoteDir . '/current', 0755, true); + } + + /** + * Sets up local and remote hosts with their paths + */ + private function setUpHosts(): void + { + // Local host setup + $this->localHost = new Localhost(); + $this->localHost->set('deploy_path', $this->localDir); + $this->localHost->set('current_path', $this->localDir . '/current'); + $this->localHost->set('release_or_current_path', $this->localDir . '/current'); + $this->localHost->set('bin/wp', 'wp'); + $this->localHost->set('bin/php', 'php'); + + // Remote host setup (using Localhost for testing) + $this->remoteHost = new Localhost('testremote'); + $this->remoteHost->set('deploy_path', $this->remoteDir); + $this->remoteHost->set('current_path', $this->remoteDir . '/current'); + $this->remoteHost->set('release_or_current_path', $this->remoteDir . '/current'); + $this->remoteHost->set('bin/wp', 'wp'); + $this->remoteHost->set('bin/php', 'php'); + + // Register hosts with Deployer + $this->deployer->hosts->set('localhost', $this->localHost); + $this->deployer->hosts->set('testremote', $this->remoteHost); + } + + /** + * Creates mock objects for SSH and process running + * And register services in Deployer container + */ + protected function setUpMockedServices(): void + { + $this->sshClient = $this->createMock(Client::class); + $this->deployer['sshClient'] = $this->sshClient; + // Server is not needed and throws errors in dev. + $this->deployer['server'] = $this->createMock(Server::class); + + // Create a mock runner but don't use it by default. + // Can't store originalProcessRunner here already, because it needs to dynamically resolve from the container later. + $this->mockedRunner = $this->createMock(ProcessRunner::class); + } + + /** + * Helper to mock specific commands while passing others through + * + * @param array $commandsToMock Map of command patterns to callables or return values + */ + protected function mockCommands(array $commandsToMock): void + { + // Set up the mock to handle specific commands + $this->mockedRunner->expects($this->any()) + ->method('run') + ->willReturnCallback(function ($host, $command, $options = []) use ($commandsToMock) { + // Check if this command should be mocked + foreach ($commandsToMock as $pattern => $handler) { + if (str_contains($command, $pattern)) { + return is_callable($handler) ? $handler($host, $command, $options) : $handler; + } + } + + // If not mocked, pass through to original runner + return $this->originalProcessRunner->run($host, $command, $options); + }); + + // Use a closure factory, because the original runner needs dependencies. + $this->deployer['processRunner'] = function ($c) { + $this->originalProcessRunner = new ProcessRunner($c['pop'], $c['logger']); + return $this->mockedRunner; + }; + } + + protected function tearDown(): void + { + $this->removeDirectory($this->testDir); + parent::tearDown(); + } + + protected function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = "$dir/$file"; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + + rmdir($dir); + } + + /** + * Returns the full path to a fixture file + */ + protected function getFixturePath(string $path): string + { + return __FIXTURES__ . '/' . $path; + } + + /** + * Runs a deployer task with the given arguments + */ + protected function dep(string $task, ?string $host = 'testremote', array $args = []): int + { + $input = [$task]; + if (!empty($host)) { + $input['selector'] = [$host]; + } + $input['--file'] = static::RECIPE_PATH; + $input = array_merge($input, $args); + return $this->tester->run($input, [ + 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, + 'interactive' => false, + ]); + } + + /** + * Helper method to mock rsync failures. Use $checkSource and $checkDestination if you want to mock only specific up-/download. + * + * @param string|null $checkSource When set, the source in the download call will be compared against and only these downloads will fail. + * @param string|null $checkDestination When set, the destination in the download call will be compared against and only these downloads will fail. + */ + protected function mockRsyncFailure(?string $checkSource = null, ?string $checkDestination = null): void + { + $this->rsyncMock = $this->createMock(Rsync::class); + $this->deployer['rsync'] = $this->rsyncMock; + $this->rsyncMock->expects($this->once()) + ->method('call') + ->willReturnCallback(function (Host $host, $source, $destination, $options) use ($checkSource, $checkDestination) { + if ($checkSource && $source !== $checkSource) { + return; + } + if ($checkDestination && $destination !== $checkSource) { + return; + } + throw new Exception('Download failed'); + }); + } +} diff --git a/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php b/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php new file mode 100644 index 0000000..224f8eb --- /dev/null +++ b/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php @@ -0,0 +1,616 @@ +dep('list', null); + $output = $this->tester->getDisplay(); + $this->assertStringContainsString('db:remote:backup', $output, 'Should list db:remote:backup task'); + $this->assertStringContainsString('db:local:backup', $output, 'Should list db:local:backup task'); + } + + public function testDbRemoteBackup(): void + { + $this->mockSuccessfulDbExport(); + + // Run the backup task + $result = $this->dep('db:remote:backup'); + $this->assertEquals(0, $result); + + // Verify dump files were created + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $this->assertCount(1, $remoteDumpFiles, 'Remote dump file should be created'); + $this->assertCount(1, $localDumpFiles, 'Local dump file should be created'); + + // Verify file contents match fixture + $fixtureContent = file_get_contents($this->getFixturePath('database/dump.sql')); + $remoteContent = file_get_contents($remoteDumpFiles[0]); + $localContent = file_get_contents($localDumpFiles[0]); + + $this->assertEquals($fixtureContent, $remoteContent, 'Remote dump file should match fixture'); + $this->assertEquals($fixtureContent, $localContent, 'Local dump file should match fixture'); + } + + /** + * Helper method to mock successful WP-CLI database export + */ + protected function mockSuccessfulDbExport(string $wpBinary = 'wp'): void + { + $this->mockCommands([ + "$wpBinary db export" => function ($host, $command) use ($wpBinary) { + if (preg_match('/db export (.*?db_backup-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}\.sql)(?:\s|$)/', $command, $matches)) { + $dumpFile = $matches[1]; + } else { + // Determine if this is a local or remote command based on the host + $dumpFile = $host->getHostname() === 'local' + ? $this->localDir . '/dumps/db_backup_' . date('Y-m-d_H-i') . '.sql' + : $this->remoteDir . '/dumps/db_backup_' . date('Y-m-d_H-i') . '.sql'; + } + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + return 'Database exported successfully'; + } + ]); + } + + public function testDbRemoteBackupWithCustomDumpPath(): void + { + // Set custom dump paths + $customRemotePath = $this->remoteDir . '/custom/dumps'; + $customLocalPath = $this->localDir . '/custom/dumps'; + + $this->remoteHost->set('dbdump/path', $customRemotePath); + $this->localHost->set('dbdump/path', $customLocalPath); + + // Create custom dump directories + mkdir($customRemotePath, 0755, true); + mkdir($customLocalPath, 0755, true); + + // Mock successful WP-CLI export + $this->mockSuccessfulDbExport(); + + // Run the backup task + $result = $this->dep('db:remote:backup'); + $this->assertEquals(0, $result); + + // Verify dump file was created in custom remote directory + $remoteDumpFiles = glob($customRemotePath . '/db_backup-*.sql'); + $this->assertCount(1, $remoteDumpFiles, 'Remote dump file should be created in custom path'); + + // Verify dump file was downloaded to custom local directory + $localDumpFiles = glob($customLocalPath . '/db_backup-*.sql'); + $this->assertCount(1, $localDumpFiles, 'Local dump file should be created in custom path'); + + // Verify file contents match fixture + $fixtureContent = file_get_contents($this->getFixturePath('database/dump.sql')); + $remoteContent = file_get_contents($remoteDumpFiles[0]); + $localContent = file_get_contents($localDumpFiles[0]); + + $this->assertEquals($fixtureContent, $remoteContent, 'Remote dump file should match fixture'); + $this->assertEquals($fixtureContent, $localContent, 'Local dump file should match fixture'); + } + + public function testDbRemoteBackupWithCustomWpBinary(): void + { + // Set custom WP-CLI binary + $this->remoteHost->set('bin/wp', '/usr/local/bin/wp-cli'); + $this->mockSuccessfulDbExport('/usr/local/bin/wp-cli'); + + // Run the backup task + $result = $this->dep('db:remote:backup'); + $this->assertEquals(0, $result); + + // Verify dump files were created + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $this->assertCount(1, $remoteDumpFiles, 'Remote dump file should be created'); + $this->assertCount(1, $localDumpFiles, 'Local dump file should be created'); + } + + public function testDbRemoteBackupWithExistingDumpDirectory(): void + { + // Create dump directory with existing files + $existingFile = $this->remoteDir . '/dumps/existing.sql'; + file_put_contents($existingFile, 'existing content'); + + // Mock successful WP-CLI export + $this->mockSuccessfulDbExport(); + + // Run the backup task + $result = $this->dep('db:remote:backup'); + $this->assertEquals(0, $result); + + // Verify existing file wasn't modified + $this->assertEquals('existing content', file_get_contents($existingFile), 'Existing file should not be modified'); + + // Verify new dump file was created + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $this->assertCount(1, $remoteDumpFiles, 'New dump file should be created'); + } + + public function testDbRemoteBackupWithWpCliError(): void + { + $this->mockFailedDbExport(); + + // Run the backup task and expect it to fail + $result = $this->dep('db:remote:backup'); + $this->assertNotEquals(0, $result, 'Task should fail when WP-CLI fails'); + + // Verify no dump files were created + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $this->assertCount(0, $remoteDumpFiles, 'No remote dump file should be created on error'); + $this->assertCount(0, $localDumpFiles, 'No local dump file should be created on error'); + } + + /** + * Helper method to mock WP-CLI database export failure + */ + protected function mockFailedDbExport(string $error = 'WP-CLI error: Database connection failed'): void + { + $this->mockCommands([ + 'wp db export' => function () use ($error) { + throw new RuntimeException($error); + } + ]); + } + + public function testDbRemoteBackupWithDownloadError(): void + { + // Mock successful export but failed download + $this->mockSuccessfulDbExport(); + $this->mockRsyncFailure(); + + // Run the backup task and expect it to fail + $result = $this->dep('db:remote:backup'); + $this->assertNotEquals(0, $result, 'Task should fail when download fails'); + + // Verify remote dump file was created but local wasn't + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $this->assertCount(1, $remoteDumpFiles, 'Remote dump file should be created even if download fails'); + $this->assertCount(0, $localDumpFiles, 'No local dump file should be created on download error'); + } + + public function testDbRemoteBackupWithInvalidDumpPath(): void + { + // Set invalid dump path (non-writable directory) + $invalidPath = '/root/invalid/path'; + $this->remoteHost->set('dbdump/path', $invalidPath); + + // Run the backup task and expect it to fail + $this->mockSuccessfulDbExport(); + $result = $this->dep('db:remote:backup'); + $this->assertNotEquals(0, $result, 'Task should fail with invalid dump path'); + $output = $this->tester->getDisplay(); + $this->assertStringContainsString('Task db:remote:backup failed', $output); + + // Verify no dump files were created + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $invalidPathFiles = glob('/root/invalid/path/db_backup-*.sql'); + $this->assertCount(0, $remoteDumpFiles, 'No remote dump file should be created with invalid path'); + $this->assertCount(0, $localDumpFiles, 'No local dump file should be created with invalid path'); + $this->assertCount(0, $invalidPathFiles, 'No root dump file should be created with invalid path'); + } + + public function testDbLocalBackup(): void + { + $this->mockSuccessfulDbExport(); + + // Run the backup task + $result = $this->dep('db:local:backup'); + $this->assertEquals(0, $result); + + // Verify dump files were created + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $this->assertCount(1, $localDumpFiles, 'Local dump file should be created'); + $this->assertCount(1, $remoteDumpFiles, 'Remote dump file should be created'); + + // Verify file contents match fixture + $fixtureContent = file_get_contents($this->getFixturePath('database/dump.sql')); + $localContent = file_get_contents($localDumpFiles[0]); + $remoteContent = file_get_contents($remoteDumpFiles[0]); + + $this->assertEquals($fixtureContent, $localContent, 'Local dump file should match fixture'); + $this->assertEquals($fixtureContent, $remoteContent, 'Remote dump file should match fixture'); + } + + public function testDbLocalBackupWithCustomDumpPath(): void + { + // Set custom dump paths + $customRemotePath = $this->remoteDir . '/custom/dumps'; + $customLocalPath = $this->localDir . '/custom/dumps'; + + $this->remoteHost->set('dbdump/path', $customRemotePath); + $this->localHost->set('dbdump/path', $customLocalPath); + + // Create custom dump directories + mkdir($customRemotePath, 0755, true); + mkdir($customLocalPath, 0755, true); + + // Mock successful WP-CLI export + $this->mockSuccessfulDbExport(); + + // Run the backup task + $result = $this->dep('db:local:backup'); + $this->assertEquals(0, $result); + + // Verify dump file was created in custom local directory + $localDumpFiles = glob($customLocalPath . '/db_backup-*.sql'); + $this->assertCount(1, $localDumpFiles, 'Local dump file should be created in custom path'); + + // Verify dump file was uploaded to custom remote directory + $remoteDumpFiles = glob($customRemotePath . '/db_backup-*.sql'); + $this->assertCount(1, $remoteDumpFiles, 'Remote dump file should be created in custom path'); + + // Verify file contents match fixture + $fixtureContent = file_get_contents($this->getFixturePath('database/dump.sql')); + $localContent = file_get_contents($localDumpFiles[0]); + $remoteContent = file_get_contents($remoteDumpFiles[0]); + + $this->assertEquals($fixtureContent, $localContent, 'Local dump file should match fixture'); + $this->assertEquals($fixtureContent, $remoteContent, 'Remote dump file should match fixture'); + } + + public function testDbLocalBackupWithCustomWpBinary(): void + { + // Set custom WP-CLI binary + $this->localHost->set('bin/wp', '/usr/local/bin/wp-cli'); + $this->mockSuccessfulDbExport('/usr/local/bin/wp-cli'); + + // Run the backup task + $result = $this->dep('db:local:backup'); + $this->assertEquals(0, $result); + + // Verify dump files were created + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $this->assertCount(1, $localDumpFiles, 'Local dump file should be created'); + $this->assertCount(1, $remoteDumpFiles, 'Remote dump file should be created'); + } + + public function testDbLocalBackupWithExistingDumpDirectory(): void + { + // Create dump directory with existing files + $existingFile = $this->remoteDir . '/dumps/existing.sql'; + file_put_contents($existingFile, 'existing content'); + + // Mock successful WP-CLI export + $this->mockSuccessfulDbExport(); + + // Run the backup task + $result = $this->dep('db:local:backup'); + $this->assertEquals(0, $result); + + // Verify existing file wasn't modified + $this->assertEquals('existing content', file_get_contents($existingFile), 'Existing file should not be modified'); + + // Verify new dump file was created + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $this->assertCount(1, $remoteDumpFiles, 'New dump file should be created'); + } + + public function testDbLocalBackupWithWpCliError(): void + { + $this->mockFailedDbExport(); + + // Run the backup task and expect it to fail + $result = $this->dep('db:local:backup'); + $this->assertNotEquals(0, $result, 'Task should fail when WP-CLI fails'); + + // Verify no dump files were created + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $this->assertCount(0, $localDumpFiles, 'No local dump file should be created on error'); + $this->assertCount(0, $remoteDumpFiles, 'No remote dump file should be created on error'); + } + + public function testDbLocalBackupWithUploadError(): void + { + // Mock successful export but failed upload + $this->mockSuccessfulDbExport(); + $this->mockRsyncFailure(); + + // Run the backup task and expect it to fail + $result = $this->dep('db:local:backup'); + $this->assertNotEquals(0, $result, 'Task should fail when upload fails'); + + // Verify local dump file was created but remote wasn't + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $this->assertCount(1, $localDumpFiles, 'Local dump file should be created even if upload fails'); + $this->assertCount(0, $remoteDumpFiles, 'No remote dump file should be created on upload error'); + } + + public function testDbLocalBackupWithInvalidDumpPath(): void + { + // Set invalid dump path (non-writable directory) + $invalidPath = '/root/invalid/path'; + $this->localHost->set('dbdump/path', $invalidPath); + + // Run the backup task and expect it to fail + $this->mockSuccessfulDbExport(); + $result = $this->dep('db:local:backup'); + $this->assertNotEquals(0, $result, 'Task should fail with invalid dump path'); + $output = $this->tester->getDisplay(); + $this->assertStringContainsString('Task db:local:backup failed', $output); + + // Verify no dump files were created + $localDumpFiles = glob($this->localDir . '/dumps/db_backup-*.sql'); + $remoteDumpFiles = glob($this->remoteDir . '/dumps/db_backup-*.sql'); + $invalidPathFiles = glob('/root/invalid/path/db_backup-*.sql'); + $this->assertCount(0, $localDumpFiles, 'No local dump file should be created with invalid path'); + $this->assertCount(0, $remoteDumpFiles, 'No remote dump file should be created with invalid path'); + $this->assertCount(0, $invalidPathFiles, 'No invalid path dump file should be created'); + } + + public function testDbRemoteImport(): void + { + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->remoteDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock successful import and URL replacement + $this->mockSuccessfulDbImport(); + + // Run the import task + $result = $this->dep('db:remote:import'); + $this->assertEquals(0, $result); + + // Verify dump file was cleaned up + $this->assertFileDoesNotExist($dumpFile, 'Dump file should be removed after import'); + } + + /** + * Helper method to mock successful database import and URL replacement + */ + protected function mockSuccessfulDbImport(): void + { + $this->mockCommands([ + 'wp db import' => function () { + return 'Database imported successfully'; + }, + 'wp search-replace' => function () { + return 'Made some replacements'; + } + ]); + } + + public function testDbRemoteImportWithUploadsPathReplacement(): void + { + // Set up URLs and upload paths for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + $this->localHost->set('uploads/dir', '/local/uploads'); + $this->remoteHost->set('uploads/dir', '/remote/uploads'); + + // Create a dump file to import + $dumpFile = $this->remoteDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock successful import and replacements + $this->mockSuccessfulDbImport(); + + // Run the import task + $result = $this->dep('db:remote:import'); + $this->assertEquals(0, $result); + + // Verify dump file was cleaned up + $this->assertFileDoesNotExist($dumpFile, 'Dump file should be removed after import'); + } + + public function testDbRemoteImportWithImportError(): void + { + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->remoteDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock failed import + $this->mockCommands([ + 'wp db import' => function () { + throw new RuntimeException('Import failed'); + } + ]); + + // Run the import task and expect failure + $result = $this->dep('db:remote:import'); + $this->assertNotEquals(0, $result, 'Task should fail when import fails'); + + // Verify dump file still exists (not cleaned up on error) + $this->assertFileExists($dumpFile, 'Dump file should remain when import fails'); + } + + public function testDbRemoteImportWithUrlReplaceError(): void + { + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->remoteDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock successful import but failed URL replacement + $this->mockCommands([ + 'wp db import' => function () { + return 'Database imported successfully'; + }, + 'wp search-replace' => function () { + throw new RuntimeException('URL replacement failed'); + } + ]); + + // Run the import task and expect failure + $result = $this->dep('db:remote:import'); + $this->assertNotEquals(0, $result, 'Task should fail when URL replacement fails'); + + // Verify dump file still exists (not cleaned up on error) + $this->assertFileExists($dumpFile, 'Dump file should remain when URL replacement fails'); + } + + public function testDbRemoteImportWithMissingDumpFile(): void + { + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Set non-existent dump file path + $dumpFile = $this->remoteDir . '/dumps/nonexistent.sql'; + set('dbdump/file', 'db_backup.sql'); + + // Run the import task and expect failure + $result = $this->dep('db:remote:import'); + $this->assertNotEquals(0, $result, 'Task should fail when dump file is missing'); + } + + public function testDbLocalImport(): void + { + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->localDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock successful import and URL replacement + $this->mockSuccessfulDbImport(); + + // Run the import task + $result = $this->dep('db:local:import'); + $this->assertEquals(0, $result); + + // Verify dump file was cleaned up + $this->assertFileDoesNotExist($dumpFile, 'Dump file should be removed after import'); + } + + public function testDbLocalImportWithUploadsPathReplacement(): void + { + // Set up URLs and upload paths for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + $this->localHost->set('uploads/dir', '/local/uploads'); + $this->remoteHost->set('uploads/dir', '/remote/uploads'); + + // Create a dump file to import + $dumpFile = $this->localDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock successful import and replacements + $this->mockSuccessfulDbImport(); + + // Run the import task + $result = $this->dep('db:local:import'); + $this->assertEquals(0, $result); + + // Verify dump file was cleaned up + $this->assertFileDoesNotExist($dumpFile, 'Dump file should be removed after import'); + } + + public function testDbLocalImportWithImportError(): void + { + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->localDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock failed import + $this->mockCommands([ + 'wp db import' => function () { + throw new RuntimeException('Import failed'); + } + ]); + + // Run the import task and expect failure + $result = $this->dep('db:local:import'); + $this->assertNotEquals(0, $result, 'Task should fail when import fails'); + + // Verify dump file still exists (not cleaned up on error) + $this->assertFileExists($dumpFile, 'Dump file should remain when import fails'); + } + + public function testDbLocalImportWithUrlReplaceError(): void + { + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->localDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock successful import but failed URL replacement + $this->mockCommands([ + 'wp db import' => function () { + return 'Database imported successfully'; + }, + 'wp search-replace' => function () { + throw new RuntimeException('URL replacement failed'); + } + ]); + + // Run the import task and expect failure + $result = $this->dep('db:local:import'); + $this->assertNotEquals(0, $result, 'Task should fail when URL replacement fails'); + + // Verify dump file still exists (not cleaned up on error) + $this->assertFileExists($dumpFile, 'Dump file should remain when URL replacement fails'); + } + + public function testDbLocalImportWithMissingDumpFile(): void + { + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Set non-existent dump file path + $dumpFile = $this->localDir . '/dumps/nonexistent.sql'; + set('dbdump/file', 'db_backup.sql'); + + // Run the import task and expect failure + $result = $this->dep('db:local:import'); + $this->assertNotEquals(0, $result, 'Task should fail when dump file is missing'); + } + + protected function setUp(): void + { + parent::setUp(); + + // Set up required configuration + $this->localHost->set('dbdump/path', $this->localDir . '/dumps'); + $this->remoteHost->set('dbdump/path', $this->remoteDir . '/dumps'); + + // Create dumps directory + mkdir($this->remoteDir . '/dumps', 0755, true); + mkdir($this->localDir . '/dumps', 0755, true); + } +} From 92302e3ef3b7fe7091d54d9e86e452ca20e2c466 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Wed, 26 Mar 2025 13:40:10 +0100 Subject: [PATCH 22/36] [WIP] functional tests for language tasks --- composer.json | 8 +- recipes/bedrock.php | 17 -- recipes/common.php | 18 +++ recipes/simple.php | 17 -- src/Files.php | 7 +- tasks/languages.php | 2 + tests/Fixtures/languages/test-de_AT.l10n.php | 2 + tests/Fixtures/languages/test-de_AT.mo | Bin 0 -> 422 bytes tests/Fixtures/languages/test-de_AT.po | 30 ++++ tests/Fixtures/languages/test.pot | 29 ++++ tests/Functional/FunctionalTestCase.php | 29 ++-- .../Tasks/LanguageTasksFunctionalTest.php | 145 ++++++++++++++++++ 12 files changed, 255 insertions(+), 49 deletions(-) create mode 100644 tests/Fixtures/languages/test-de_AT.l10n.php create mode 100644 tests/Fixtures/languages/test-de_AT.mo create mode 100644 tests/Fixtures/languages/test-de_AT.po create mode 100644 tests/Fixtures/languages/test.pot create mode 100644 tests/Functional/Tasks/LanguageTasksFunctionalTest.php diff --git a/composer.json b/composer.json index 0740f14..bfb5dea 100644 --- a/composer.json +++ b/composer.json @@ -45,9 +45,9 @@ "tests:functional": "DO_NOT_TRACK=true vendor/bin/phpunit --testsuite Functional", "tests:functional:coverage": "DO_NOT_TRACK=true XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite Functional --coverage-text", "tests": [ - "@test:unit", - "@test:integration", - "@test:functional" + "@tests:unit", + "@tests:integration", + "@tests:functional" ], "tests:coverage": "DO_NOT_TRACK=true XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text", "precommit": [ @@ -55,7 +55,7 @@ "@phpcs", "@phpstan", "@composer validate", - "@test" + "@tests" ] }, "config": { diff --git a/recipes/bedrock.php b/recipes/bedrock.php index fbf0fd8..6b7c01e 100644 --- a/recipes/bedrock.php +++ b/recipes/bedrock.php @@ -24,23 +24,6 @@ add('recipes', ['bedrock-wp']); -// Use fixed release_path always -set('release_or_current_path', function () { - return get('release_path'); -}); - -// Use a dummy current_path because deployer checks if it's a symlink -set('current_path', function () { - if (test('[ ! -f {{deploy_path}}/.dep/current ]')) { - run('{{bin/symlink}} {{release_path}} {{deploy_path}}/.dep/current'); - } - return '{{deploy_path}}/.dep/current'; -}); - -// Do not use shared dirs -set('shared_files', []); -set('shared_dirs', []); - // Tasks task('app:push', function () { $rsyncOptions = Rsync::buildOptionsArray(); diff --git a/recipes/common.php b/recipes/common.php index c11179c..bb2a05b 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -47,6 +47,7 @@ // Include task definitions require __DIR__ . '/../tasks/database.php'; require __DIR__ . '/../tasks/files.php'; +require __DIR__ . '/../tasks/languages.php'; require __DIR__ . '/../tasks/mu-plugins.php'; require __DIR__ . '/../tasks/packages.php'; require __DIR__ . '/../tasks/plugins.php'; @@ -110,6 +111,23 @@ // PATHS & FILES CONFIGURATION +// Use fixed release_path always +set('release_or_current_path', function () { + return '{{release_path}}'; // Do not use get() to stay in same context. +}); + +// Use a dummy current_path because deployer checks if it's a symlink +set('current_path', function () { + if (test('[ ! -f {{deploy_path}}/.dep/current ]')) { + run('{{bin/symlink}} {{release_path}} {{deploy_path}}/.dep/current'); + } + return '{{deploy_path}}/.dep/current'; +}); + +// Do not use shared dirs +set('shared_files', []); +set('shared_dirs', []); + // if you want to further define options for rsyncing files // just look at the source in `Files.php` and `Rsync.php` // and use the Rsync::buildOptionsArray and Files::push/pull methods diff --git a/recipes/simple.php b/recipes/simple.php index 0f98611..9e5cf4a 100644 --- a/recipes/simple.php +++ b/recipes/simple.php @@ -19,23 +19,6 @@ add('recipes', ['simple-wp']); -// Use fixed release_path always -set('release_or_current_path', function () { - return get('release_path'); -}); - -// Use a dummy current_path because deployer checks if it's a symlink -set('current_path', function () { - if (test('[ ! -f {{deploy_path}}/.dep/current ]')) { - run('{{bin/symlink}} {{release_path}} {{deploy_path}}/.dep/current'); - } - return '{{deploy_path}}/.dep/current'; -}); - -// Do not use shared dirs -set('shared_files', []); -set('shared_dirs', []); - task('deploy:update_code', ['packages:push']) ->desc('Pushes local packages to the remote hosts'); diff --git a/src/Files.php b/src/Files.php index a26c91e..99d4fae 100644 --- a/src/Files.php +++ b/src/Files.php @@ -4,6 +4,7 @@ use function Deployer\download; use function Deployer\run; +use function Deployer\runLocally; use function Deployer\upload; /** @@ -20,7 +21,8 @@ class Files */ public static function pushFiles(string $localPath, string $remotePath, array $rsyncOptions = []): void { - $localPath = Localhost::getConfig('current_path') . '/' . $localPath; + $localPath = Localhost::getConfig('release_or_current_path') . '/' . $localPath; + run("mkdir -p {{release_or_current_path}}/$remotePath"); // Always ensure remote directory exists upload($localPath . '/', '{{release_or_current_path}}/' . $remotePath . '/', ['options' => $rsyncOptions]); } @@ -33,7 +35,8 @@ public static function pushFiles(string $localPath, string $remotePath, array $r */ public static function pullFiles(string $remotePath, string $localPath, array $rsyncOptions = []): void { - $localPath = Localhost::getConfig('current_path') . '/' . $localPath; + $localPath = Localhost::getConfig('release_or_current_path') . '/' . $localPath; + runLocally("mkdir -p $localPath"); // Always ensure directory exists. download('{{release_or_current_path}}/' . $remotePath . '/', $localPath . '/', ['options' => $rsyncOptions]); } diff --git a/tasks/languages.php b/tasks/languages.php index c4753ca..0d11b6f 100644 --- a/tasks/languages.php +++ b/tasks/languages.php @@ -18,6 +18,7 @@ use function Deployer\download; use function Deployer\get; +use function Deployer\run; use function Deployer\task; /** @@ -51,6 +52,7 @@ $rsyncOptions = Rsync::buildOptionsArray([ 'filter' => get("languages/filter"), ]); + run('mkdir -p {{release_or_current_path}}/{{languages/dir}}'); // Always ensure remote directory exists. Files::pullFiles('{{languages/dir}}', Localhost::getConfig('languages/dir'), $rsyncOptions); })->desc('Pull languages from remote to local'); diff --git a/tests/Fixtures/languages/test-de_AT.l10n.php b/tests/Fixtures/languages/test-de_AT.l10n.php new file mode 100644 index 0000000..1aa162a --- /dev/null +++ b/tests/Fixtures/languages/test-de_AT.l10n.php @@ -0,0 +1,2 @@ +'dummy-plugin','plural-forms'=>NULL,'language'=>'de','project-id-version'=>'Dummy Plugin 1.0.0','pot-creation-date'=>'2025-03-26T08:12:34+00:00','po-revision-date'=>'2025-03-26 13:38+0100','x-generator'=>'Poedit 3.5','messages'=>['Dummy Plugin'=>'Test Plugin','A dummy test plugin'=>'Ein test Plugin']]; \ No newline at end of file diff --git a/tests/Fixtures/languages/test-de_AT.mo b/tests/Fixtures/languages/test-de_AT.mo new file mode 100644 index 0000000000000000000000000000000000000000..ef871eba816dc349520125b6d4ceeb6a2ae93a52 GIT binary patch literal 422 zcmYk2&r8EF6vyi?genMn^^m({W~-A?@gTO!V8bF+JQ~+$jkHNgQ*l?1-o5z8`M3D( zI@lih$lCGLN?jMy85=TGbl%9EcuKNdyN4_ze>$GVD_uzW z*G4T9)L$?{mwOr~@mveAoj{u93?O}6_) iVklFN1-&8akts0`@~sWL^ThXJr8|?BzND>T%kw{eDsTq? literal 0 HcmV?d00001 diff --git a/tests/Fixtures/languages/test-de_AT.po b/tests/Fixtures/languages/test-de_AT.po new file mode 100644 index 0000000..1fdcc90 --- /dev/null +++ b/tests/Fixtures/languages/test-de_AT.po @@ -0,0 +1,30 @@ +# Copyright (C) 2025 Fabian Todt +# This file is distributed under the GPLv3+. +msgid "" +msgstr "" +"Project-Id-Version: Dummy Plugin 1.0.0\n" +"POT-Creation-Date: 2025-03-26T08:12:34+00:00\n" +"PO-Revision-Date: 2025-03-26 13:38+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.5\n" +"X-Domain: dummy-plugin\n" + +#. Plugin Name of the plugin +#: dummy-plugin.php +msgid "Dummy Plugin" +msgstr "Test Plugin" + +#. Description of the plugin +#: dummy-plugin.php +msgid "A dummy test plugin" +msgstr "Ein test Plugin" + +#. Author of the plugin +#: dummy-plugin.php +msgid "Fabian Todt" +msgstr "" diff --git a/tests/Fixtures/languages/test.pot b/tests/Fixtures/languages/test.pot new file mode 100644 index 0000000..0b8ce41 --- /dev/null +++ b/tests/Fixtures/languages/test.pot @@ -0,0 +1,29 @@ +# Copyright (C) 2025 Fabian Todt +# This file is distributed under the GPLv3+. +msgid "" +msgstr "" +"Project-Id-Version: Dummy Plugin 1.0.0\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2025-03-26T08:12:34+00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"X-Generator: WP-CLI 2.11.0\n" +"X-Domain: dummy-plugin\n" + +#. Plugin Name of the plugin +#: dummy-plugin.php +msgid "Dummy Plugin" +msgstr "" + +#. Description of the plugin +#: dummy-plugin.php +msgid "A dummy test plugin" +msgstr "" + +#. Author of the plugin +#: dummy-plugin.php +msgid "Fabian Todt" +msgstr "" \ No newline at end of file diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php index 2b7a42c..f99211d 100644 --- a/tests/Functional/FunctionalTestCase.php +++ b/tests/Functional/FunctionalTestCase.php @@ -30,15 +30,17 @@ abstract class FunctionalTestCase extends TestCase protected const RECIPE_PATH = __DIR__ . '/../Fixtures/recipes/common.php'; protected Deployer $deployer; protected ApplicationTester $tester; - protected Host $localHost; // Test environment paths + protected Host $localHost; protected Host $remoteHost; protected string $testDir; protected string $localDir; + protected string $localReleaseDir; + protected string $remoteDir; + protected string $remoteReleaseDir; // Mocked services - protected string $remoteDir; protected Client|MockObject $sshClient; protected ProcessRunner $originalProcessRunner; @@ -73,13 +75,19 @@ protected function setUp(): void private function createTestDirectories(): void { $this->testDir = __TEMP_DIR__ . '/' . uniqid(); + + // These are the deploy_path. $this->localDir = $this->testDir . '/local'; $this->remoteDir = $this->testDir . '/remote'; + // Fixed directory, no symlinks. Just like in example deploy.yml + $this->localReleaseDir = $this->localDir . '/public_html'; + $this->remoteReleaseDir = $this->remoteDir . '/public_html'; + mkdir($this->localDir, 0755, true); - mkdir($this->localDir . '/current', 0755, true); + mkdir($this->localReleaseDir, 0755, true); mkdir($this->remoteDir, 0755, true); - mkdir($this->remoteDir . '/current', 0755, true); + mkdir($this->remoteReleaseDir, 0755, true); } /** @@ -88,18 +96,21 @@ private function createTestDirectories(): void private function setUpHosts(): void { // Local host setup + // This mirrors the setup in examples/simple/deploy.php $this->localHost = new Localhost(); $this->localHost->set('deploy_path', $this->localDir); - $this->localHost->set('current_path', $this->localDir . '/current'); - $this->localHost->set('release_or_current_path', $this->localDir . '/current'); + $this->localHost->set('release_path', $this->localReleaseDir); + $this->localHost->set('dbdump/path', $this->localDir . '/data/db_dumps'); + $this->localHost->set('backup_path', $this->localDir . '/data/backups'); $this->localHost->set('bin/wp', 'wp'); $this->localHost->set('bin/php', 'php'); - // Remote host setup (using Localhost for testing) + // Remote host setup (using Localhost for testing so rsync runs on the same host) $this->remoteHost = new Localhost('testremote'); $this->remoteHost->set('deploy_path', $this->remoteDir); - $this->remoteHost->set('current_path', $this->remoteDir . '/current'); - $this->remoteHost->set('release_or_current_path', $this->remoteDir . '/current'); + $this->remoteHost->set('release_path', $this->remoteReleaseDir); + $this->remoteHost->set('dbdump/path', $this->remoteDir . '/data/db_dumps'); + $this->remoteHost->set('backup_path', $this->remoteDir . '/data/backups'); $this->remoteHost->set('bin/wp', 'wp'); $this->remoteHost->set('bin/php', 'php'); diff --git a/tests/Functional/Tasks/LanguageTasksFunctionalTest.php b/tests/Functional/Tasks/LanguageTasksFunctionalTest.php new file mode 100644 index 0000000..57e5889 --- /dev/null +++ b/tests/Functional/Tasks/LanguageTasksFunctionalTest.php @@ -0,0 +1,145 @@ +localLanguagesDir = $this->localReleaseDir . '/wp-content/languages'; + $this->remoteLanguagesDir = $this->remoteReleaseDir . '/wp-content/languages'; + $this->localBackupDir = $this->localDir . '/backups'; + $this->remoteBackupDir = $this->remoteDir . '/backups'; + + // Create directories + mkdir($this->localLanguagesDir, 0755, true); + mkdir($this->remoteLanguagesDir, 0755, true); + mkdir($this->localBackupDir, 0755, true); + mkdir($this->remoteBackupDir, 0755, true); + + // Configure paths in deployer + // $this->deployer->config->set('languages/dir', 'wp-content/languages'); // Use default one set in recipe. + $this->deployer->config->set('backup_path', $this->remoteBackupDir); + + // Configure localhost + $this->localHost->set('backup_path', $this->localBackupDir); + } + + public function testListAvailableTasks(): void + { + $this->dep('list', null); + $output = $this->tester->getDisplay(); + + $this->assertStringContainsString('languages:push', $output); + $this->assertStringContainsString('languages:pull', $output); + $this->assertStringContainsString('languages:sync', $output); + $this->assertStringContainsString('languages:backup:remote', $output); + $this->assertStringContainsString('languages:backup:local', $output); + } + + public function testLanguagesPush(): void + { + $languageFiles = glob($this->getFixturePath('languages/*')); + foreach ($languageFiles as $languageFile) { + copy($languageFile, $this->localLanguagesDir . '/' . basename($languageFile)); + } + + $result = $this->dep('languages:push'); + $this->assertEquals(0, $result); + + foreach ($languageFiles as $languageFile) { + $filePath = $this->remoteLanguagesDir . '/' . basename($languageFile); + $originalFilePath = $this->localLanguagesDir . '/' . basename($languageFile); + $this->assertFileExists($filePath); + $this->assertEquals( + file_get_contents($originalFilePath), + file_get_contents($filePath) + ); + } + } + + public function testLanguagesPushWithCustomFilter(): void + { + $this->deployer->config->set('languages/filter', ['- *.po']); + $languageFiles = glob($this->getFixturePath('languages/*')); + foreach ($languageFiles as $languageFile) { + copy($languageFile, $this->localLanguagesDir . '/' . basename($languageFile)); + } + + $result = $this->dep('languages:push'); + $this->assertEquals(0, $result); + + $remoteLanguageFiles = glob($this->remoteLanguagesDir . '/*'); + $this->assertCount(3, $remoteLanguageFiles); // 4 - 1 .po file. + foreach ($languageFiles as $languageFile) { + if (str_ends_with($languageFile, '.po')) { + continue; + } + $filePath = $this->remoteLanguagesDir . '/' . basename($languageFile); + $originalFilePath = $this->localLanguagesDir . '/' . basename($languageFile); + $this->assertFileExists($filePath); + $this->assertEquals( + file_get_contents($originalFilePath), + file_get_contents($filePath) + ); + } + } + + public function testLanguagesPull(): void + { + $languageFiles = glob($this->getFixturePath('languages/*')); + foreach ($languageFiles as $languageFile) { + copy($languageFile, $this->remoteLanguagesDir . '/' . basename($languageFile)); + } + + $result = $this->dep('languages:pull'); + $this->assertEquals(0, $result); + + foreach ($languageFiles as $languageFile) { + $filePath = $this->localLanguagesDir . '/' . basename($languageFile); + $originalFilePath = $this->remoteLanguagesDir . '/' . basename($languageFile); + $this->assertFileExists($filePath); + $this->assertEquals( + file_get_contents($originalFilePath), + file_get_contents($filePath) + ); + } + } + + public function testLanguagesPullWithCustomFilter(): void + { + $this->deployer->config->set('languages/filter', ['- *.po']); + $languageFiles = glob($this->getFixturePath('languages/*')); + foreach ($languageFiles as $languageFile) { + copy($languageFile, $this->remoteLanguagesDir . '/' . basename($languageFile)); + } + + $result = $this->dep('languages:pull'); + $this->assertEquals(0, $result); + + $localLanguageFiles = glob($this->localLanguagesDir . '/*'); + $this->assertCount(3, $localLanguageFiles); // 4 - 1 .po file. + foreach ($languageFiles as $languageFile) { + if (str_ends_with($languageFile, '.po')) { + continue; + } + $filePath = $this->localLanguagesDir . '/' . basename($languageFile); + $originalFilePath = $this->remoteLanguagesDir . '/' . basename($languageFile); + $this->assertFileExists($filePath); + $this->assertEquals( + file_get_contents($originalFilePath), + file_get_contents($filePath) + ); + } + } +} From 5cefbee0b4aafc48385992c2a83ad036acee93b8 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Tue, 1 Apr 2025 16:03:10 +0200 Subject: [PATCH 23/36] added a localhost:run wrapper and renamed localhost path options --- examples/bedrock/deploy.php | 13 ++----- examples/bedrock/deploy.yml | 2 +- examples/simple/deploy.php | 11 ++---- examples/simple/deploy.yml | 2 +- recipes/common.php | 12 +++--- src/Files.php | 24 ++++++------ src/Localhost.php | 31 ++++++++++++++- src/WPCLI.php | 17 ++++---- tasks/database.php | 39 +++++++++---------- tests/Functional/FunctionalTestCase.php | 14 +++---- .../Tasks/DatabaseTasksFunctionalTest.php | 16 ++++---- .../Tasks/LanguageTasksFunctionalTest.php | 2 +- 12 files changed, 102 insertions(+), 81 deletions(-) diff --git a/examples/bedrock/deploy.php b/examples/bedrock/deploy.php index 62150b1..8151bdb 100644 --- a/examples/bedrock/deploy.php +++ b/examples/bedrock/deploy.php @@ -19,21 +19,16 @@ // OPTIONAL: overwrite localhost config. localhost() ->set('public_url', "{{local_url}}") - ->set('deploy_path', __DIR__) - // Root project directory, for app:push to work. - ->set('release_path', __DIR__) - // set current_path to hardcoded release_path on local so release_or_current_path works; - // {{release_path}} does not work here? - ->set('current_path', function () { - return Localhost::getConfig('release_path'); - }) + ->set('project_path', __DIR__) + ->set('current_path', __DIR__) // Bedrock dirs + ->set('uploads/path', '{{current_path}}') ->set('uploads/dir', 'web/app/uploads') ->set('mu-plugins/dir', 'web/app/mu-plugins') ->set('themes/dir', 'web/app/themes') ->set('plugins/dir', 'web/app/plugins') ->set('wp/dir', 'web/wp') - ->set('dbdump/path', __DIR__ . '/data/db_dumps') + ->set('dbdump_path', __DIR__ . '/data/db_dumps') ->set('backup_path', __DIR__ . '/data/backups'); set('packages', [ diff --git a/examples/bedrock/deploy.yml b/examples/bedrock/deploy.yml index 7ab42e4..9970955 100644 --- a/examples/bedrock/deploy.yml +++ b/examples/bedrock/deploy.yml @@ -14,5 +14,5 @@ hosts: themes/dir: web/app/themes plugins/dir: web/app/plugins wp/dir: web/wp - dbdump/path: ~/data/dumps + dbdump_path: ~/data/dumps backup_path: ~/data/backups diff --git a/examples/simple/deploy.php b/examples/simple/deploy.php index 26b60bc..8ea5dcf 100644 --- a/examples/simple/deploy.php +++ b/examples/simple/deploy.php @@ -19,14 +19,9 @@ // OPTIONAL: overwrite localhost config. localhost() ->set('public_url', "{{local_url}}") - ->set('deploy_path', __DIR__) - ->set('release_path', __DIR__ . '/public') - // set current_path to hardcoded release_path on local so release_or_current_path works; - // {{release_path}} does not work here? - ->set('current_path', function () { - return Localhost::getConfig('release_path'); - }) - ->set('dbdump/path', __DIR__ . '/data/db_dumps') + ->set('project_path', __DIR__) + ->set('current_path', 'public') // The public doc root as kind of the currents release path. + ->set('dbdump_path', __DIR__ . '/data/db_dumps') ->set('backup_path', __DIR__ . '/data/backups'); set('packages', [ diff --git a/examples/simple/deploy.yml b/examples/simple/deploy.yml index 0602b08..d7fb6ce 100644 --- a/examples/simple/deploy.yml +++ b/examples/simple/deploy.yml @@ -10,5 +10,5 @@ hosts: public_url: https://test.dev deploy_path: "~" release_path: "{{deploy_path}}/public_html" # fixed directory, no symlinks - dbdump/path: ~/data/dumps + dbdump_path: ~/data/dumps backup_path: ~/data/backups diff --git a/recipes/common.php b/recipes/common.php index bb2a05b..f520d4b 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -124,10 +124,6 @@ return '{{deploy_path}}/.dep/current'; }); -// Do not use shared dirs -set('shared_files', []); -set('shared_dirs', []); - // if you want to further define options for rsyncing files // just look at the source in `Files.php` and `Rsync.php` // and use the Rsync::buildOptionsArray and Files::push/pull methods @@ -186,8 +182,12 @@ set('zip_options', '-x "_backup_*.zip" -x **/node_modules/**\* -x **/vendor/**\*'); // SHARED FILES -set('shared_files', ['wp-config.php', 'wp-config-local.php']); -set('shared_dirs', ['{{uploads/dir}}']); +// Do not use shared dirs +set('shared_files', []); +set('shared_dirs', []); +# If you are using symlinked deployments, enable these +// set('shared_files', ['wp-config.php', 'wp-config-local.php']); +// set('shared_dirs', ['{{uploads/dir}}']); set('writable_dirs', ['{{uploads/dir}}']); // The default rsync config diff --git a/src/Files.php b/src/Files.php index 99d4fae..ab459ac 100644 --- a/src/Files.php +++ b/src/Files.php @@ -4,7 +4,6 @@ use function Deployer\download; use function Deployer\run; -use function Deployer\runLocally; use function Deployer\upload; /** @@ -13,30 +12,33 @@ class Files { /** - * Push files from local to remote - * @param string $localPath Local path to push from - * @param string $remotePath Remote path to push to + * Push files from local to remote. Always works from the localhosts current_path directory. + * If you want to upload files outside of current_path, use upload() directly. + * + * @param string $localPath Local path to push from, relative to localhosts current_path. + * @param string $remotePath Remote path to push to, relative to remote hosts release_or_current_path. * @param RsyncOptions $rsyncOptions Rsync options array * @return void */ public static function pushFiles(string $localPath, string $remotePath, array $rsyncOptions = []): void { - $localPath = Localhost::getConfig('release_or_current_path') . '/' . $localPath; - run("mkdir -p {{release_or_current_path}}/$remotePath"); // Always ensure remote directory exists + $localPath = Localhost::getConfig('current_path') . '/' . $localPath; + run("mkdir -p {{release_or_current_path}}/$remotePath"); // Always ensure remote directory exists. upload($localPath . '/', '{{release_or_current_path}}/' . $remotePath . '/', ['options' => $rsyncOptions]); } /** - * Pull files from remote to local - * @param string $remotePath Remote path to pull from - * @param string $localPath Local path to pull to + * Pull files from remote to local. Always works from the localhosts current_path directory. + * If you want to download files outside of current_path, use download() directly. + * @param string $remotePath Remote path to pull from, relative to remote hosts release_or_current_path. + * @param string $localPath Local path to pull to, relative to localhosts current_path. * @param RsyncOptions $rsyncOptions Rsync options array * @return void */ public static function pullFiles(string $remotePath, string $localPath, array $rsyncOptions = []): void { - $localPath = Localhost::getConfig('release_or_current_path') . '/' . $localPath; - runLocally("mkdir -p $localPath"); // Always ensure directory exists. + $localPath = Localhost::getConfig('current_path') . '/' . $localPath; + Localhost::run("mkdir -p $localPath"); // Always ensure directory exists. download('{{release_or_current_path}}/' . $remotePath . '/', $localPath . '/', ['options' => $rsyncOptions]); } diff --git a/src/Localhost.php b/src/Localhost.php index 0b1c01e..66df9d8 100644 --- a/src/Localhost.php +++ b/src/Localhost.php @@ -2,8 +2,12 @@ namespace Gaambo\DeployerWordpress; +use Deployer\Configuration\Configuration; use Deployer\Deployer; use Deployer\Host\Host; +use Deployer\Task\Context; + +use function Deployer\runLocally; /** * Localhost utility class @@ -28,6 +32,31 @@ public static function getConfig(string $key): mixed */ public static function get(): Host { - return Deployer::get()->hosts->get('localhost'); + $localhost = Deployer::get()->hosts->get('localhost'); + return $localhost; + } + + /** + * A wrapper around runLocally() which parses variables from the local host. + * The problem with runLocally() is that it parses values from the global context which + * does not use our Localhost instance. + * + * @param string $command Command to run on localhost. + * @param string[]|null $options Array of options will override passed named arguments. + * @return string + */ + public static function run(string $command, ?array $options = []): string + { + // Let's build a configuration object that has all the localhost values + // But has the current context and global deployer context as parents to fallback. + $config = new Configuration(); + $config->update(self::get()->config()->ownValues()); + if (Context::has()) { + $config->bind(Context::get()->getConfig()); + } else { + $config->bind(Deployer::get()->config); + } + $command = $config->parse($command); + return runLocally($command, $options); } } diff --git a/src/WPCLI.php b/src/WPCLI.php index 1201907..424ebe6 100644 --- a/src/WPCLI.php +++ b/src/WPCLI.php @@ -3,7 +3,6 @@ namespace Gaambo\DeployerWordpress; use function Deployer\run; -use function Deployer\runLocally; /** * WP CLI utility class @@ -12,7 +11,6 @@ class WPCLI { private const BINARY_DOWNLOAD = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar'; - private const DEFAULT_PATH = '{{release_or_current_path}}'; /** * Run a WP CLI command @@ -21,8 +19,11 @@ class WPCLI * @param string $arguments Additional arguments to pass to WP-CLI * @return void */ - public static function runCommand(string $command, ?string $path = self::DEFAULT_PATH, string $arguments = ''): void - { + public static function runCommand( + string $command, + ?string $path = '{{release_or_current_path}}', + string $arguments = '' + ): void { $cmd = "{{bin/wp}} $command $arguments"; if ($path) { run("cd $path && $cmd"); @@ -34,20 +35,20 @@ public static function runCommand(string $command, ?string $path = self::DEFAULT /** * Run a WP CLI command locally * @param string $command The command to run (without wp prefix) - * @param string|null $path The path to run the command in (defaults to {{release_or_current_path}}) + * @param string|null $path The path to run the command in (defaults to null on local host) * @param string $arguments Additional arguments to pass to WP-CLI * @return void */ public static function runCommandLocally( string $command, - ?string $path = self::DEFAULT_PATH, + ?string $path = null, string $arguments = '' ): void { $localWp = Localhost::getConfig('bin/wp'); if ($path) { - runLocally("cd $path && $localWp $command $arguments"); + Localhost::run("cd $path && $localWp $command $arguments"); } else { - runLocally("$localWp $command $arguments"); + Localhost::run("$localWp $command $arguments"); } } diff --git a/tasks/database.php b/tasks/database.php index 9f47511..d90ded8 100644 --- a/tasks/database.php +++ b/tasks/database.php @@ -20,7 +20,6 @@ use function Deployer\get; use function Deployer\has; use function Deployer\run; -use function Deployer\runLocally; use function Deployer\set; use function Deployer\task; use function Deployer\test; @@ -31,22 +30,22 @@ * Create backup of remote database and download locally * * Configuration: - * - dbdump/path: Directory to store database dumps (required on both local and remote) + * - dbdump_path: Directory to store database dumps (required on both local and remote) * - bin/wp: WP-CLI binary/command to use (automatically configured) * * Example: * dep db:remote:backup prod */ task('db:remote:backup', function () { - $localDumpPath = Localhost::getConfig('dbdump/path'); - $remoteDumpPath = get('dbdump/path'); + $localDumpPath = Localhost::getConfig('dbdump_path'); + $remoteDumpPath = get('dbdump_path'); $now = date('Y-m-d_H-i', time()); set('dbdump/file', "db_backup-$now.sql"); - run('mkdir -p ' . get('dbdump/path')); + run('mkdir -p ' . get('dbdump_path')); WPCLI::runCommand("db export $remoteDumpPath/{{dbdump/file}} --add-drop-table", "{{release_or_current_path}}"); - runLocally("mkdir -p $localDumpPath"); + Localhost::run("mkdir -p $localDumpPath"); download("$remoteDumpPath/{{dbdump/file}}", "$localDumpPath/{{dbdump/file}}"); })->desc('Create backup of remote database and download locally'); @@ -54,22 +53,22 @@ * Create backup of local database and upload to remote * * Configuration: - * - dbdump/path: Directory to store database dumps (required on both local and remote) + * - dbdump_path: Directory to store database dumps (required on both local and remote) * - bin/wp: WP-CLI binary/command to use (automatically configured) * * Example: * dep db:local:backup prod */ task('db:local:backup', function () { - $localDumpPath = Localhost::getConfig('dbdump/path'); - $remoteDumpPath = get('dbdump/path'); + $localDumpPath = Localhost::getConfig('dbdump_path'); + $remoteDumpPath = get('dbdump_path'); $now = date('Y-m-d_H-i', time()); set('dbdump/file', "db_backup-$now.sql"); - runLocally("mkdir -p $localDumpPath"); + Localhost::run("mkdir -p $localDumpPath"); WPCLI::runCommandLocally("db export $localDumpPath/{{dbdump/file}} --add-drop-table"); - run('mkdir -p {{dbdump/path}}'); + run('mkdir -p {{dbdump_path}}'); upload( "$localDumpPath/{{dbdump/file}}", "$remoteDumpPath/{{dbdump/file}}" @@ -89,22 +88,22 @@ */ task('db:remote:import', function () { // Check if dump file exists - if (!has('dbdump/file') || !test('[ -f {{dbdump/path}}/{{dbdump/file}} ]')) { - throw new \RuntimeException('Database dump file not found at {{dbdump/path}}/{{dbdump/file}}'); + if (!has('dbdump/file') || !test('[ -f {{dbdump_path}}/{{dbdump/file}} ]')) { + throw new \RuntimeException('Database dump file not found at {{dbdump_path}}/{{dbdump/file}}'); } $localUrl = Localhost::getConfig('public_url'); - WPCLI::runCommand("db import {{dbdump/path}}/{{dbdump/file}}", "{{release_or_current_path}}"); - WPCLI::runCommand("search-replace $localUrl {{public_url}}", "{{release_or_current_path}}"); + WPCLI::runCommand("db import {{dbdump_path}}/{{dbdump/file}}"); + WPCLI::runCommand("search-replace $localUrl {{public_url}}"); // If the local uploads directory is different from the remote one // replace all references to the local uploads directory with the remote one $localUploadsDir = Localhost::getConfig('uploads/dir'); if ($localUploadsDir !== get('uploads/dir')) { - WPCLI::runCommand("search-replace $localUploadsDir {{uploads/dir}}", "{{release_or_current_path}}"); + WPCLI::runCommand("search-replace $localUploadsDir {{uploads/dir}}"); } - run('rm -f {{dbdump/path}}/{{dbdump/file}}'); + run('rm -f {{dbdump_path}}/{{dbdump/file}}'); })->desc('Import database backup on remote host'); /** @@ -114,14 +113,14 @@ * - bin/wp: WP-CLI binary/command to use (automatically configured) * - public_url: Site URL for both local and remote (required for URL replacement) * - uploads/dir: Upload directory path (for path replacement if different between environments) - * - dbdump/path: Directory containing database dumps + * - dbdump_path: Directory containing database dumps * * Example: * dep db:local:import prod */ task('db:local:import', function () { // Check if dump file exists - $localDumpPath = Localhost::getConfig('dbdump/path'); + $localDumpPath = Localhost::getConfig('dbdump_path'); if (!has('dbdump/file') || !testLocally("[ -f $localDumpPath/{{dbdump/file}} ]")) { throw new \RuntimeException("Database dump file not found at $localDumpPath/{{dbdump/file}}"); } @@ -136,7 +135,7 @@ WPCLI::runCommandLocally("search-replace {{uploads/dir}} $localUploadsDir"); } - runLocally("rm -f $localDumpPath/{{dbdump/file}}"); + Localhost::run("rm -f $localDumpPath/{{dbdump/file}}"); })->desc('Import database backup on local host'); /** diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php index f99211d..13101a3 100644 --- a/tests/Functional/FunctionalTestCase.php +++ b/tests/Functional/FunctionalTestCase.php @@ -36,7 +36,7 @@ abstract class FunctionalTestCase extends TestCase protected Host $remoteHost; protected string $testDir; protected string $localDir; - protected string $localReleaseDir; + protected string $localDocRootDir; protected string $remoteDir; protected string $remoteReleaseDir; @@ -81,11 +81,11 @@ private function createTestDirectories(): void $this->remoteDir = $this->testDir . '/remote'; // Fixed directory, no symlinks. Just like in example deploy.yml - $this->localReleaseDir = $this->localDir . '/public_html'; + $this->localDocRootDir = $this->localDir . '/public_html'; $this->remoteReleaseDir = $this->remoteDir . '/public_html'; mkdir($this->localDir, 0755, true); - mkdir($this->localReleaseDir, 0755, true); + mkdir($this->localDocRootDir, 0755, true); mkdir($this->remoteDir, 0755, true); mkdir($this->remoteReleaseDir, 0755, true); } @@ -98,9 +98,9 @@ private function setUpHosts(): void // Local host setup // This mirrors the setup in examples/simple/deploy.php $this->localHost = new Localhost(); - $this->localHost->set('deploy_path', $this->localDir); - $this->localHost->set('release_path', $this->localReleaseDir); - $this->localHost->set('dbdump/path', $this->localDir . '/data/db_dumps'); + $this->localHost->set('project_path', $this->localDir); + $this->localHost->set('current_path', $this->localDocRootDir); + $this->localHost->set('dbdump_path', $this->localDir . '/data/db_dumps'); $this->localHost->set('backup_path', $this->localDir . '/data/backups'); $this->localHost->set('bin/wp', 'wp'); $this->localHost->set('bin/php', 'php'); @@ -109,7 +109,7 @@ private function setUpHosts(): void $this->remoteHost = new Localhost('testremote'); $this->remoteHost->set('deploy_path', $this->remoteDir); $this->remoteHost->set('release_path', $this->remoteReleaseDir); - $this->remoteHost->set('dbdump/path', $this->remoteDir . '/data/db_dumps'); + $this->remoteHost->set('dbdump_path', $this->remoteDir . '/data/db_dumps'); $this->remoteHost->set('backup_path', $this->remoteDir . '/data/backups'); $this->remoteHost->set('bin/wp', 'wp'); $this->remoteHost->set('bin/php', 'php'); diff --git a/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php b/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php index 224f8eb..32d91a9 100644 --- a/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php +++ b/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php @@ -67,8 +67,8 @@ public function testDbRemoteBackupWithCustomDumpPath(): void $customRemotePath = $this->remoteDir . '/custom/dumps'; $customLocalPath = $this->localDir . '/custom/dumps'; - $this->remoteHost->set('dbdump/path', $customRemotePath); - $this->localHost->set('dbdump/path', $customLocalPath); + $this->remoteHost->set('dbdump_path', $customRemotePath); + $this->localHost->set('dbdump_path', $customLocalPath); // Create custom dump directories mkdir($customRemotePath, 0755, true); @@ -184,7 +184,7 @@ public function testDbRemoteBackupWithInvalidDumpPath(): void { // Set invalid dump path (non-writable directory) $invalidPath = '/root/invalid/path'; - $this->remoteHost->set('dbdump/path', $invalidPath); + $this->remoteHost->set('dbdump_path', $invalidPath); // Run the backup task and expect it to fail $this->mockSuccessfulDbExport(); @@ -231,8 +231,8 @@ public function testDbLocalBackupWithCustomDumpPath(): void $customRemotePath = $this->remoteDir . '/custom/dumps'; $customLocalPath = $this->localDir . '/custom/dumps'; - $this->remoteHost->set('dbdump/path', $customRemotePath); - $this->localHost->set('dbdump/path', $customLocalPath); + $this->remoteHost->set('dbdump_path', $customRemotePath); + $this->localHost->set('dbdump_path', $customLocalPath); // Create custom dump directories mkdir($customRemotePath, 0755, true); @@ -336,7 +336,7 @@ public function testDbLocalBackupWithInvalidDumpPath(): void { // Set invalid dump path (non-writable directory) $invalidPath = '/root/invalid/path'; - $this->localHost->set('dbdump/path', $invalidPath); + $this->localHost->set('dbdump_path', $invalidPath); // Run the backup task and expect it to fail $this->mockSuccessfulDbExport(); @@ -606,8 +606,8 @@ protected function setUp(): void parent::setUp(); // Set up required configuration - $this->localHost->set('dbdump/path', $this->localDir . '/dumps'); - $this->remoteHost->set('dbdump/path', $this->remoteDir . '/dumps'); + $this->localHost->set('dbdump_path', $this->localDir . '/dumps'); + $this->remoteHost->set('dbdump_path', $this->remoteDir . '/dumps'); // Create dumps directory mkdir($this->remoteDir . '/dumps', 0755, true); diff --git a/tests/Functional/Tasks/LanguageTasksFunctionalTest.php b/tests/Functional/Tasks/LanguageTasksFunctionalTest.php index 57e5889..8805789 100644 --- a/tests/Functional/Tasks/LanguageTasksFunctionalTest.php +++ b/tests/Functional/Tasks/LanguageTasksFunctionalTest.php @@ -16,7 +16,7 @@ protected function setUp(): void parent::setUp(); // Set up temporary directories - $this->localLanguagesDir = $this->localReleaseDir . '/wp-content/languages'; + $this->localLanguagesDir = $this->localDocRootDir . '/wp-content/languages'; $this->remoteLanguagesDir = $this->remoteReleaseDir . '/wp-content/languages'; $this->localBackupDir = $this->localDir . '/backups'; $this->remoteBackupDir = $this->remoteDir . '/backups'; From 6e2d3ce53ece03c38c0ea25935d0b07fb3455d98 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Tue, 1 Apr 2025 16:03:21 +0200 Subject: [PATCH 24/36] add language backup tasks tests --- .../Tasks/LanguageTasksFunctionalTest.php | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/tests/Functional/Tasks/LanguageTasksFunctionalTest.php b/tests/Functional/Tasks/LanguageTasksFunctionalTest.php index 8805789..be6f320 100644 --- a/tests/Functional/Tasks/LanguageTasksFunctionalTest.php +++ b/tests/Functional/Tasks/LanguageTasksFunctionalTest.php @@ -142,4 +142,130 @@ public function testLanguagesPullWithCustomFilter(): void ); } } + + public function testLanguagesBackupRemote(): void + { + // Copy language files to remote + $languageFiles = glob($this->getFixturePath('languages/*')); + foreach ($languageFiles as $languageFile) { + copy($languageFile, $this->remoteLanguagesDir . '/' . basename($languageFile)); + } + + // Run backup task + $result = $this->dep('languages:backup:remote'); + $this->assertEquals(0, $result); + + // Check if backup file exists locally + $backupFiles = glob($this->localBackupDir . '/backup_languages_*.zip'); + $this->assertCount(1, $backupFiles, 'Backup file should exist'); + + // Extract backup to verify contents + $backupFile = $backupFiles[0]; + $extractDir = $this->localBackupDir . '/extracted'; + mkdir($extractDir, 0755, true); + + $zip = new \ZipArchive(); + $this->assertTrue($zip->open($backupFile)); + $zip->extractTo($extractDir); + $zip->close(); + + // Verify all language files are in the backup + foreach ($languageFiles as $languageFile) { + $fileName = basename($languageFile); + $this->assertFileExists($extractDir . '/' . $fileName); + $this->assertEquals( + file_get_contents($languageFile), + file_get_contents($extractDir . '/' . $fileName) + ); + } + + // Cleanup + $this->removeDirectory($extractDir); + } + + public function testLanguagesBackupRemoteWithCustomBackupPath(): void + { + // Set custom backup path + $customBackupPath = $this->remoteDir . '/custom_backups'; + mkdir($customBackupPath, 0755, true); + $this->deployer->config->set('backup_path', $customBackupPath); + $this->localHost->set('backup_path', $this->localDir . '/custom_backups'); + mkdir($this->localDir . '/custom_backups', 0755, true); + + // Copy language files to remote + $languageFiles = glob($this->getFixturePath('languages/*')); + foreach ($languageFiles as $languageFile) { + copy($languageFile, $this->remoteLanguagesDir . '/' . basename($languageFile)); + } + + // Run backup task + $result = $this->dep('languages:backup:remote'); + $this->assertEquals(0, $result); + + // Check if backup file exists in custom location + $backupFiles = glob($this->localDir . '/custom_backups/backup_languages_*.zip'); + $this->assertCount(1, $backupFiles, 'Backup file should exist in custom location'); + } + + public function testLanguagesBackupLocal(): void + { + // Copy language files to local + $languageFiles = glob($this->getFixturePath('languages/*')); + foreach ($languageFiles as $languageFile) { + copy($languageFile, $this->localLanguagesDir . '/' . basename($languageFile)); + } + + // Run backup task + $result = $this->dep('languages:backup:local'); + $this->assertEquals(0, $result); + + // Check if backup file exists locally + $backupFiles = glob($this->localBackupDir . '/backup_languages_*.zip'); + $this->assertCount(1, $backupFiles, 'Backup file should exist'); + + // Extract backup to verify contents + $backupFile = $backupFiles[0]; + $extractDir = $this->localBackupDir . '/extracted'; + mkdir($extractDir, 0755, true); + + $zip = new \ZipArchive(); + $this->assertTrue($zip->open($backupFile)); + $zip->extractTo($extractDir); + $zip->close(); + + // Verify all language files are in the backup + foreach ($languageFiles as $languageFile) { + $fileName = basename($languageFile); + $this->assertFileExists($extractDir . '/' . $fileName); + $this->assertEquals( + file_get_contents($languageFile), + file_get_contents($extractDir . '/' . $fileName) + ); + } + + // Cleanup + $this->removeDirectory($extractDir); + } + + public function testLanguagesBackupLocalWithCustomBackupPath(): void + { + // Set custom backup path + $customBackupPath = $this->localDir . '/custom_backups'; + mkdir($customBackupPath, 0755, true); + $this->localHost->set('backup_path', $customBackupPath); + + // Copy language files to local + $languageFiles = glob($this->getFixturePath('languages/*')); + foreach ($languageFiles as $languageFile) { + copy($languageFile, $this->localLanguagesDir . '/' . basename($languageFile)); + } + + // Run backup task + $result = $this->dep('languages:backup:local'); + $this->assertEquals(0, $result); + + // Check if backup file exists in custom location + $backupFiles = glob($customBackupPath . '/backup_languages_*.zip'); + $this->assertCount(1, $backupFiles, 'Backup file should exist in custom location'); + } } From 65d648f317e0709114323242dad12792148207e7 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Tue, 1 Apr 2025 16:04:20 +0200 Subject: [PATCH 25/36] fix wpcli runcommand locally test --- tests/Integration/WpCliIntegrationTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/WpCliIntegrationTest.php b/tests/Integration/WpCliIntegrationTest.php index 0808c86..f876514 100644 --- a/tests/Integration/WpCliIntegrationTest.php +++ b/tests/Integration/WpCliIntegrationTest.php @@ -57,7 +57,7 @@ public function testRunCommandLocally(): void ->method('run') ->with( $this->anything(), // Host object is not reliable as runLocally creates a new instance - 'cd /var/www/current && wp post list --format=table ', + 'wp post list --format=table ', [] ); @@ -346,4 +346,4 @@ public function testInstallWithCustomBinaryPath(): void $result = WPCLI::install($installPath, $binaryName, true); $this->assertEquals("$installPath/$binaryName", $result); } -} +} From 7877ee20370132174b7991b3935fba3bd4e1a32a Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Tue, 1 Apr 2025 17:16:35 +0200 Subject: [PATCH 26/36] remove :sync tasks and add tests for mu-plugins --- tasks/files.php | 12 - tasks/languages.php | 12 - tasks/mu-plugins.php | 12 - tasks/packages.php | 12 - tasks/plugins.php | 12 - tasks/themes.php | 12 - tasks/uploads.php | 12 - .../mu-plugins/test-plugin/composer.json | 7 + .../mu-plugins/test-plugin/test-plugin.php | 15 + .../mu-plugins/test-plugin/test-plugin.txt | 5 + tests/Functional/FunctionalTestCase.php | 26 ++ .../Tasks/MuPluginsTasksFunctionalTest.php | 347 ++++++++++++++++++ 12 files changed, 400 insertions(+), 84 deletions(-) create mode 100644 tests/Fixtures/mu-plugins/test-plugin/composer.json create mode 100644 tests/Fixtures/mu-plugins/test-plugin/test-plugin.php create mode 100644 tests/Fixtures/mu-plugins/test-plugin/test-plugin.txt create mode 100644 tests/Functional/Tasks/MuPluginsTasksFunctionalTest.php diff --git a/tasks/files.php b/tasks/files.php index e6c607a..ab9d55d 100644 --- a/tasks/files.php +++ b/tasks/files.php @@ -50,18 +50,6 @@ task('files:pull', ['wp:pull', 'uploads:pull', 'plugins:pull', 'mu-plugins:pull', 'themes:pull', 'packages:pull']) ->desc('Pull all files from remote to local'); -/** - * Sync all files between remote and local - * - * Combines files:push and files:pull tasks. - * See individual tasks for configuration options. - * - * Example: - * dep files:sync prod - */ -task('files:sync', ['files:push', 'files:pull']) - ->desc('Sync all files between environments'); - /** * Backup all files on remote host * diff --git a/tasks/languages.php b/tasks/languages.php index 0d11b6f..a78db04 100644 --- a/tasks/languages.php +++ b/tasks/languages.php @@ -56,18 +56,6 @@ Files::pullFiles('{{languages/dir}}', Localhost::getConfig('languages/dir'), $rsyncOptions); })->desc('Pull languages from remote to local'); -/** - * Sync languages between remote and local - * - * Combines languages:push and languages:pull tasks. - * See individual tasks for configuration options. - * - * Example: - * dep languages:sync prod - */ -task('languages:sync', ['languages:push', 'languages:pull']) - ->desc('Sync languages between environments'); - /** * Backup languages on remote host * diff --git a/tasks/mu-plugins.php b/tasks/mu-plugins.php index 3686006..68422ee 100644 --- a/tasks/mu-plugins.php +++ b/tasks/mu-plugins.php @@ -83,18 +83,6 @@ Files::pullFiles('{{mu-plugins/dir}}', Localhost::getConfig('mu-plugins/dir'), $rsyncOptions); })->desc('Pull mu-plugins from remote to local'); -/** - * Sync mu-plugins between remote and local - * - * Combines mu-plugins:push and mu-plugins:pull tasks. - * See individual tasks for configuration options. - * - * Example: - * dep mu-plugins:sync prod - */ -task('mu-plugins:sync', ['mu-plugins:push', 'mu-plugins:pull']) - ->desc('Sync mu-plugins between environments'); - /** * Backup mu-plugins on remote host * diff --git a/tasks/packages.php b/tasks/packages.php index 3d89f7d..085be0e 100644 --- a/tasks/packages.php +++ b/tasks/packages.php @@ -157,18 +157,6 @@ } })->desc('Pull packages from remote to local'); -/** - * Sync packages between remote and local - * - * Combines packages:push and packages:pull tasks. - * See individual tasks for configuration options. - * - * Example: - * dep packages:sync prod - */ -task('packages:sync', ['packages:push', 'packages:pull']) - ->desc('Sync packages between environments'); - /** * Backup packages on remote host * diff --git a/tasks/plugins.php b/tasks/plugins.php index 6c0931f..fea7703 100644 --- a/tasks/plugins.php +++ b/tasks/plugins.php @@ -54,18 +54,6 @@ Files::pullFiles('{{plugins/dir}}', Localhost::getConfig('plugins/dir'), $rsyncOptions); })->desc('Pull plugins from remote to local'); -/** - * Sync plugins between remote and local - * - * Combines plugins:push and plugins:pull tasks. - * See individual tasks for configuration options. - * - * Example: - * dep plugins:sync prod - */ -task('plugins:sync', ['plugins:push', 'plugins:pull']) - ->desc('Sync plugins between environments'); - /** * Backup plugins on remote host * diff --git a/tasks/themes.php b/tasks/themes.php index 885a465..559c91a 100644 --- a/tasks/themes.php +++ b/tasks/themes.php @@ -142,18 +142,6 @@ Files::pullFiles('{{themes/dir}}', Localhost::getConfig('themes/dir'), $rsyncOptions); })->desc('Pull themes from remote to local'); -/** - * Sync themes between remote and local - * - * Combines themes:push and themes:pull tasks. - * See individual tasks for configuration options. - * - * Example: - * dep themes:sync prod - */ -task('themes:sync', ['themes:push', 'themes:pull']) - ->desc('Sync themes between environments'); - /** * Backup themes on remote host * diff --git a/tasks/uploads.php b/tasks/uploads.php index c102e43..b406819 100644 --- a/tasks/uploads.php +++ b/tasks/uploads.php @@ -66,18 +66,6 @@ download("{{uploads/path}}/{{uploads/dir}}/", "$localUploadsPath/$localUploadsDir/", ['options' => $rsyncOptions]); })->desc('Pull uploads from remote to local'); -/** - * Sync uploads between remote and local - * - * Combines uploads:push and uploads:pull tasks. - * See individual tasks for configuration options. - * - * Example: - * dep uploads:sync prod - */ -task('uploads:sync', ['uploads:push', 'uploads:pull']) - ->desc('Sync uploads between environments'); - /** * Backup uploads on remote host * diff --git a/tests/Fixtures/mu-plugins/test-plugin/composer.json b/tests/Fixtures/mu-plugins/test-plugin/composer.json new file mode 100644 index 0000000..45e8fce --- /dev/null +++ b/tests/Fixtures/mu-plugins/test-plugin/composer.json @@ -0,0 +1,7 @@ +{ + "name": "test/plugin", + "type": "wordpress-plugin", + "require": { + "php": ">=8.1" + } +} \ No newline at end of file diff --git a/tests/Fixtures/mu-plugins/test-plugin/test-plugin.php b/tests/Fixtures/mu-plugins/test-plugin/test-plugin.php new file mode 100644 index 0000000..d2531e9 --- /dev/null +++ b/tests/Fixtures/mu-plugins/test-plugin/test-plugin.php @@ -0,0 +1,15 @@ +getPathname(), strlen($source) + 1); + $targetPath = $destination . DIRECTORY_SEPARATOR . $relativePath; + + if ($item->isDir()) { + mkdir($targetPath); + } else { + copy($item, $targetPath); + } + } + } + /** * Returns the full path to a fixture file */ diff --git a/tests/Functional/Tasks/MuPluginsTasksFunctionalTest.php b/tests/Functional/Tasks/MuPluginsTasksFunctionalTest.php new file mode 100644 index 0000000..d703736 --- /dev/null +++ b/tests/Functional/Tasks/MuPluginsTasksFunctionalTest.php @@ -0,0 +1,347 @@ +localMuPluginsDir = $this->localDocRootDir . '/wp-content/mu-plugins'; + $this->remoteMuPluginsDir = $this->remoteReleaseDir . '/wp-content/mu-plugins'; + $this->localBackupDir = $this->localDir . '/backups'; + $this->remoteBackupDir = $this->remoteDir . '/backups'; + + // Create directories + mkdir($this->localMuPluginsDir, 0755, true); + mkdir($this->remoteMuPluginsDir, 0755, true); + mkdir($this->localBackupDir, 0755, true); + mkdir($this->remoteBackupDir, 0755, true); + + // Configure paths in deployer + $this->deployer->config->set('mu-plugins/dir', 'wp-content/mu-plugins'); + $this->deployer->config->set('mu-plugin/name', 'test-plugin'); + $this->deployer->config->set('backup_path', $this->remoteBackupDir); + + // Configure localhost + $this->localHost->set('backup_path', $this->localBackupDir); + } + + public function testListAvailableTasks(): void + { + $this->dep('list', null); + $output = $this->tester->getDisplay(); + + $this->assertStringContainsString('mu-plugins:push', $output); + $this->assertStringContainsString('mu-plugins:pull', $output); + $this->assertStringContainsString('mu-plugins:backup:remote', $output); + $this->assertStringContainsString('mu-plugins:backup:local', $output); + $this->assertStringContainsString('mu-plugin:vendors', $output); + } + + public function testMuPluginsPush(): void + { + // Copy test plugin from fixtures to local + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Run push task + $result = $this->dep('mu-plugins:push'); + $this->assertEquals(0, $result); + + // Verify plugin directory was pushed correctly + $this->assertDirectoryExists($this->remoteMuPluginsDir . '/test-plugin'); + $this->assertFileExists($this->remoteMuPluginsDir . '/test-plugin/test-plugin.php'); + $this->assertFileExists($this->remoteMuPluginsDir . '/test-plugin/composer.json'); + } + + public function testMuPluginsPushWithCustomFilter(): void + { + // Copy test plugin from fixtures to local + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + $this->deployer->config->set('mu-plugins/filter', ['- *.json']); + + $result = $this->dep('mu-plugins:push'); + $this->assertEquals(0, $result); + + // Verify only PHP files were pushed + $this->assertDirectoryExists($this->remoteMuPluginsDir . '/test-plugin'); + $this->assertFileExists($this->remoteMuPluginsDir . '/test-plugin/test-plugin.php'); + $this->assertFileDoesNotExist($this->remoteMuPluginsDir . '/test-plugin/composer.json'); + } + + public function testMuPluginsPull(): void + { + // Set up remote directory structure + $remotePluginDir = $this->remoteMuPluginsDir . '/test-plugin'; + mkdir($remotePluginDir, 0755, true); + + // Copy from fixtures to remote + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $this->copyDirectory($fixturePluginDir, $remotePluginDir); + + // Run pull task + $result = $this->dep('mu-plugins:pull'); + $this->assertEquals(0, $result); + + // Verify plugin directory was pulled correctly + $this->assertDirectoryExists($this->localMuPluginsDir . '/test-plugin'); + $this->assertFileExists($this->localMuPluginsDir . '/test-plugin/test-plugin.php'); + $this->assertFileExists($this->localMuPluginsDir . '/test-plugin/composer.json'); + } + + public function testMuPluginsPullWithCustomFilter(): void + { + $this->deployer->config->set('mu-plugins/filter', ['- *.json']); + + // Set up remote directory structure + $remotePluginDir = $this->remoteMuPluginsDir . '/test-plugin'; + mkdir($remotePluginDir, 0755, true); + + // Copy from fixtures to remote + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $this->copyDirectory($fixturePluginDir, $remotePluginDir); + + // Run pull task + $result = $this->dep('mu-plugins:pull'); + $this->assertEquals(0, $result); + + // Verify only PHP files were pulled + $this->assertDirectoryExists($this->localMuPluginsDir . '/test-plugin'); + $this->assertFileExists($this->localMuPluginsDir . '/test-plugin/test-plugin.php'); + $this->assertFileDoesNotExist($this->localMuPluginsDir . '/test-plugin/composer.json'); + } + + public function testMuPluginsBackupRemote(): void + { + // Copy from fixtures to remote first + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $targetPluginDir = $this->remoteMuPluginsDir . '/test-plugin'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Run backup task + $result = $this->dep('mu-plugins:backup:remote'); + $this->assertEquals(0, $result); + + // Check if backup file exists locally + $backupFiles = glob($this->localBackupDir . '/backup_mu-plugins_*.zip'); + $this->assertCount(1, $backupFiles, 'Backup file should exist'); + + // Extract backup to verify contents + $backupFile = $backupFiles[0]; + $extractDir = $this->localBackupDir . '/extracted'; + mkdir($extractDir, 0755, true); + + $zip = new \ZipArchive(); + $this->assertTrue($zip->open($backupFile)); + $zip->extractTo($extractDir); + $zip->close(); + + // Verify plugin files are in the backup + $this->assertDirectoryExists($extractDir . '/test-plugin'); + $this->assertFileExists($extractDir . '/test-plugin/test-plugin.php'); + $this->assertFileExists($extractDir . '/test-plugin/composer.json'); + + // Cleanup + $this->removeDirectory($extractDir); + } + + public function testMuPluginsBackupRemoteWithCustomBackupPath(): void + { + // Set custom backup path + $customBackupPath = $this->remoteDir . '/custom_backups'; + mkdir($customBackupPath, 0755, true); + $this->deployer->config->set('backup_path', $customBackupPath); + $this->localHost->set('backup_path', $this->localDir . '/custom_backups'); + mkdir($this->localDir . '/custom_backups', 0755, true); + + // Copy from fixtures to remote first + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $targetPluginDir = $this->remoteMuPluginsDir . '/test-plugin'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Run backup task + $result = $this->dep('mu-plugins:backup:remote'); + $this->assertEquals(0, $result); + + // Check if backup file exists in custom location + $backupFiles = glob($this->localDir . '/custom_backups/backup_mu-plugins_*.zip'); + $this->assertCount(1, $backupFiles, 'Backup file should exist in custom location'); + + // Extract backup to verify contents + $backupFile = $backupFiles[0]; + $extractDir = $this->localDir . '/custom_backups/extracted'; + mkdir($extractDir, 0755, true); + + $zip = new \ZipArchive(); + $this->assertTrue($zip->open($backupFile)); + $zip->extractTo($extractDir); + $zip->close(); + + // Verify plugin files are in the backup + $this->assertDirectoryExists($extractDir . '/test-plugin'); + $this->assertFileExists($extractDir . '/test-plugin/test-plugin.php'); + $this->assertFileExists($extractDir . '/test-plugin/composer.json'); + + // Cleanup + $this->removeDirectory($extractDir); + } + + public function testMuPluginsBackupLocal(): void + { + // Copy test plugin from fixtures to local + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Run backup task + $result = $this->dep('mu-plugins:backup:local'); + $this->assertEquals(0, $result); + + // Check if backup file exists + $backupFiles = glob($this->localBackupDir . '/backup_mu-plugins_*.zip'); + $this->assertCount(1, $backupFiles, 'Backup file should exist'); + + // Extract backup to verify contents + $backupFile = $backupFiles[0]; + $extractDir = $this->localBackupDir . '/extracted'; + mkdir($extractDir, 0755, true); + + $zip = new \ZipArchive(); + $this->assertTrue($zip->open($backupFile)); + $zip->extractTo($extractDir); + $zip->close(); + + // Verify plugin files are in the backup + $this->assertDirectoryExists($extractDir . '/test-plugin'); + $this->assertFileExists($extractDir . '/test-plugin/test-plugin.php'); + $this->assertFileExists($extractDir . '/test-plugin/composer.json'); + + // Cleanup + $this->removeDirectory($extractDir); + } + + public function testMuPluginsBackupLocalWithCustomBackupPath(): void + { + // Set custom backup path + $customBackupPath = $this->localDir . '/custom_backups'; + mkdir($customBackupPath, 0755, true); + $this->localHost->set('backup_path', $customBackupPath); + + // Copy test plugin from fixtures to local + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Run backup task + $result = $this->dep('mu-plugins:backup:local'); + $this->assertEquals(0, $result); + + // Check if backup file exists in custom location + $backupFiles = glob($customBackupPath . '/backup_mu-plugins_*.zip'); + $this->assertCount(1, $backupFiles, 'Backup file should exist in custom location'); + + // Extract backup to verify contents + $backupFile = $backupFiles[0]; + $extractDir = $customBackupPath . '/extracted'; + mkdir($extractDir, 0755, true); + + $zip = new \ZipArchive(); + $this->assertTrue($zip->open($backupFile)); + $zip->extractTo($extractDir); + $zip->close(); + + // Verify plugin files are in the backup + $this->assertDirectoryExists($extractDir . '/test-plugin'); + $this->assertFileExists($extractDir . '/test-plugin/test-plugin.php'); + $this->assertFileExists($extractDir . '/test-plugin/composer.json'); + + // Cleanup + $this->removeDirectory($extractDir); + } + + public function testMuPluginVendors(): void + { + // Set default configuration + $this->deployer->config->set('mu-plugin/name', 'test-plugin'); + $this->deployer->config->set('composer_action', 'install'); + $this->deployer->config->set('composer_options', '--no-dev --no-interaction'); + $this->deployer->config->set('bin/composer', 'composer'); + + // Copy directory plugin from fixtures to target location + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $targetPluginDir = $this->remoteMuPluginsDir . '/test-plugin'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Mock composer command execution + $this->mockCommands([ + 'cd ' . $this->remoteReleaseDir . '/wp-content/mu-plugins/test-plugin && composer install --no-dev --no-interaction -v' => function () { + // Create composer.lock file with empty JSON object + $lockFile = $this->remoteReleaseDir . '/wp-content/mu-plugins/test-plugin/composer.lock'; + file_put_contents($lockFile, '{}'); + return 'Composer dependencies installed successfully'; + } + ]); + + // Run the vendors task + $result = $this->dep('mu-plugin:vendors'); + + // Verify task execution + $this->assertEquals(0, $result); + $this->assertFileExists($this->remoteReleaseDir . '/wp-content/mu-plugins/test-plugin/composer.lock'); + } + + public function testMuPluginVendorsWithCustomPaths(): void + { + // Set custom configuration + $this->deployer->config->set('mu-plugins/dir', 'wp-content/custom-mu-plugins'); + $this->deployer->config->set('mu-plugin/name', 'my-custom-plugin'); + $this->deployer->config->set('composer_action', 'install'); + $this->deployer->config->set('composer_options', '--no-dev --no-interaction'); + $this->deployer->config->set('bin/composer', 'composer'); + + // Copy directory plugin from fixtures to custom location + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + $targetPluginDir = $this->remoteMuPluginsDir . '/../custom-mu-plugins/my-custom-plugin'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Mock composer command execution + $expectedCommand = 'cd ' . $this->remoteReleaseDir . '/wp-content/custom-mu-plugins/my-custom-plugin && composer install --no-dev --no-interaction -v'; + $this->mockCommands([ + $expectedCommand => function () { + // Create composer.lock file with empty JSON object + $lockFile = $this->remoteReleaseDir . '/wp-content/custom-mu-plugins/my-custom-plugin/composer.lock'; + file_put_contents($lockFile, '{}'); + return 'Composer dependencies installed successfully'; + } + ]); + + // Run the vendors task + $result = $this->dep('mu-plugin:vendors'); + + // Verify task execution + $this->assertEquals(0, $result); + $this->assertFileExists($this->remoteReleaseDir . '/wp-content/custom-mu-plugins/my-custom-plugin/composer.lock'); + } +} From 3e20934a70d494cacb6a98b81c390e4d570f15a1 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Wed, 2 Apr 2025 16:14:05 +0200 Subject: [PATCH 27/36] remove sync task tests --- tests/Integration/Tasks/FilesTasksRegistrationTest.php | 2 -- tests/Integration/Tasks/LanguagesTasksRegistrationTest.php | 5 +++-- tests/Integration/Tasks/MuPluginsTasksRegistrationTest.php | 2 -- tests/Integration/Tasks/PackagesTasksRegistrationTest.php | 2 -- tests/Integration/Tasks/PluginsTasksRegistrationTest.php | 5 +++-- tests/Integration/Tasks/ThemesTasksRegistrationTest.php | 2 -- tests/Integration/Tasks/UploadsTasksRegistrationTest.php | 5 +++-- 7 files changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/Integration/Tasks/FilesTasksRegistrationTest.php b/tests/Integration/Tasks/FilesTasksRegistrationTest.php index 2c0be53..13a1902 100644 --- a/tests/Integration/Tasks/FilesTasksRegistrationTest.php +++ b/tests/Integration/Tasks/FilesTasksRegistrationTest.php @@ -15,7 +15,6 @@ public function testTasksAreRegistered(): void // Verify files tasks are registered $this->assertTaskExists('files:push'); $this->assertTaskExists('files:pull'); - $this->assertTaskExists('files:sync'); $this->assertTaskExists('files:backup:remote'); $this->assertTaskExists('files:backup:local'); } @@ -39,6 +38,5 @@ public function testTaskDependencies(): void 'themes:pull', 'packages:pull' ]); - $this->assertTaskDependencies('files:sync', ['files:push', 'files:pull']); } } diff --git a/tests/Integration/Tasks/LanguagesTasksRegistrationTest.php b/tests/Integration/Tasks/LanguagesTasksRegistrationTest.php index ca39d6c..7f8774a 100644 --- a/tests/Integration/Tasks/LanguagesTasksRegistrationTest.php +++ b/tests/Integration/Tasks/LanguagesTasksRegistrationTest.php @@ -2,6 +2,8 @@ namespace Gaambo\DeployerWordpress\Tests\Integration\Tasks; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; + class LanguagesTasksRegistrationTest extends TaskRegistrationTestCase { protected static function loadTasks(): void @@ -15,14 +17,13 @@ public function testTasksAreRegistered(): void // Verify languages tasks are registered $this->assertTaskExists('languages:push'); $this->assertTaskExists('languages:pull'); - $this->assertTaskExists('languages:sync'); $this->assertTaskExists('languages:backup:remote'); $this->assertTaskExists('languages:backup:local'); } + #[DoesNotPerformAssertions] public function testTaskDependencies(): void { // Verify task dependencies - $this->assertTaskDependencies('languages:sync', ['languages:push', 'languages:pull']); } } diff --git a/tests/Integration/Tasks/MuPluginsTasksRegistrationTest.php b/tests/Integration/Tasks/MuPluginsTasksRegistrationTest.php index 0d727f0..d1be74d 100644 --- a/tests/Integration/Tasks/MuPluginsTasksRegistrationTest.php +++ b/tests/Integration/Tasks/MuPluginsTasksRegistrationTest.php @@ -17,7 +17,6 @@ public function testTasksAreRegistered(): void $this->assertTaskExists('mu-plugin'); $this->assertTaskExists('mu-plugins:push'); $this->assertTaskExists('mu-plugins:pull'); - $this->assertTaskExists('mu-plugins:sync'); $this->assertTaskExists('mu-plugins:backup:remote'); $this->assertTaskExists('mu-plugins:backup:local'); } @@ -26,6 +25,5 @@ public function testTaskDependencies(): void { // Verify task dependencies $this->assertTaskDependencies('mu-plugin', ['mu-plugin:vendors']); - $this->assertTaskDependencies('mu-plugins:sync', ['mu-plugins:push', 'mu-plugins:pull']); } } diff --git a/tests/Integration/Tasks/PackagesTasksRegistrationTest.php b/tests/Integration/Tasks/PackagesTasksRegistrationTest.php index 1f0e8bd..12672ba 100644 --- a/tests/Integration/Tasks/PackagesTasksRegistrationTest.php +++ b/tests/Integration/Tasks/PackagesTasksRegistrationTest.php @@ -20,7 +20,6 @@ public function testTasksAreRegistered(): void $this->assertTaskExists('packages'); $this->assertTaskExists('packages:push'); $this->assertTaskExists('packages:pull'); - $this->assertTaskExists('packages:sync'); $this->assertTaskExists('packages:backup:remote'); $this->assertTaskExists('packages:backup:local'); } @@ -30,6 +29,5 @@ public function testTaskDependencies(): void // Verify task dependencies $this->assertTaskDependencies('packages:assets', ['packages:assets:vendors', 'packages:assets:build']); $this->assertTaskDependencies('packages', ['packages:assets', 'packages:vendors']); - $this->assertTaskDependencies('packages:sync', ['packages:push', 'packages:pull']); } } diff --git a/tests/Integration/Tasks/PluginsTasksRegistrationTest.php b/tests/Integration/Tasks/PluginsTasksRegistrationTest.php index e335339..efa4dbf 100644 --- a/tests/Integration/Tasks/PluginsTasksRegistrationTest.php +++ b/tests/Integration/Tasks/PluginsTasksRegistrationTest.php @@ -2,6 +2,8 @@ namespace Gaambo\DeployerWordpress\Tests\Integration\Tasks; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; + class PluginsTasksRegistrationTest extends TaskRegistrationTestCase { protected static function loadTasks(): void @@ -15,14 +17,13 @@ public function testTasksAreRegistered(): void // Verify plugins tasks are registered $this->assertTaskExists('plugins:push'); $this->assertTaskExists('plugins:pull'); - $this->assertTaskExists('plugins:sync'); $this->assertTaskExists('plugins:backup:remote'); $this->assertTaskExists('plugins:backup:local'); } + #[DoesNotPerformAssertions] public function testTaskDependencies(): void { // Verify task dependencies - $this->assertTaskDependencies('plugins:sync', ['plugins:push', 'plugins:pull']); } } diff --git a/tests/Integration/Tasks/ThemesTasksRegistrationTest.php b/tests/Integration/Tasks/ThemesTasksRegistrationTest.php index 1ce5d98..3bf0dd1 100644 --- a/tests/Integration/Tasks/ThemesTasksRegistrationTest.php +++ b/tests/Integration/Tasks/ThemesTasksRegistrationTest.php @@ -20,7 +20,6 @@ public function testTasksAreRegistered(): void $this->assertTaskExists('theme'); $this->assertTaskExists('themes:push'); $this->assertTaskExists('themes:pull'); - $this->assertTaskExists('themes:sync'); $this->assertTaskExists('themes:backup:remote'); $this->assertTaskExists('themes:backup:local'); } @@ -30,6 +29,5 @@ public function testTaskDependencies(): void // Verify task dependencies $this->assertTaskDependencies('theme:assets', ['theme:assets:vendors', 'theme:assets:build']); $this->assertTaskDependencies('theme', ['theme:assets', 'theme:vendors']); - $this->assertTaskDependencies('themes:sync', ['themes:push', 'themes:pull']); } } diff --git a/tests/Integration/Tasks/UploadsTasksRegistrationTest.php b/tests/Integration/Tasks/UploadsTasksRegistrationTest.php index d7202aa..d02d775 100644 --- a/tests/Integration/Tasks/UploadsTasksRegistrationTest.php +++ b/tests/Integration/Tasks/UploadsTasksRegistrationTest.php @@ -2,6 +2,8 @@ namespace Gaambo\DeployerWordpress\Tests\Integration\Tasks; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; + class UploadsTasksRegistrationTest extends TaskRegistrationTestCase { protected static function loadTasks(): void @@ -15,14 +17,13 @@ public function testTasksAreRegistered(): void // Verify uploads tasks are registered $this->assertTaskExists('uploads:push'); $this->assertTaskExists('uploads:pull'); - $this->assertTaskExists('uploads:sync'); $this->assertTaskExists('uploads:backup:remote'); $this->assertTaskExists('uploads:backup:local'); } + #[DoesNotPerformAssertions] public function testTaskDependencies(): void { // Verify task dependencies - $this->assertTaskDependencies('uploads:sync', ['uploads:push', 'uploads:pull']); } } From 053bcb63e0f7e3c94b0d95d29c2e466b12994ce6 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Wed, 2 Apr 2025 16:14:15 +0200 Subject: [PATCH 28/36] add html coverage reports --- .gitignore | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e493d4d..7042aef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ .DS_STORE .phpunit.cache .phpunit.result.cache -/coverage \ No newline at end of file +tests/coverage diff --git a/composer.json b/composer.json index bfb5dea..4c7d85a 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "@tests:integration", "@tests:functional" ], - "tests:coverage": "DO_NOT_TRACK=true XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text", + "tests:coverage": "DO_NOT_TRACK=true XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html=tests/coverage", "precommit": [ "@lint", "@phpcs", From a1562e09fa74d6a4e426d64028df68c7d6015fd5 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Wed, 2 Apr 2025 16:46:12 +0200 Subject: [PATCH 29/36] [WIP] refactor to always use "current_path" on all hosts and overwrite everything deployer does to symlink/use release_path etc. --- README.md | 127 ++++++++++++------ examples/bedrock/deploy.php | 2 +- examples/bedrock/deploy.yml | 2 +- examples/simple/deploy.yml | 2 +- recipes/bedrock.php | 5 +- recipes/common.php | 58 +++++++- src/Files.php | 3 +- src/Localhost.php | 17 +-- src/NPM.php | 8 +- tasks/database.php | 16 ++- tasks/packages.php | 1 - tests/Functional/FunctionalTestCase.php | 2 +- .../Tasks/LanguageTasksFunctionalTest.php | 1 - tests/Integration/NPMIntegrationTest.php | 124 +---------------- 14 files changed, 161 insertions(+), 207 deletions(-) diff --git a/README.md b/README.md index 58e29cf..765724b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Deployer WordPress Recipes -A collection of [Deployer](https://deployer.org) Tasks/Recipes to deploy WordPress sites. From simple sites deployed via copying files up to building custom themes/mu-plugins and installing npm/composer vendors - It handles all kinds of WordPress installation types. +A collection of [Deployer](https://deployer.org) Tasks/Recipes to deploy WordPress sites. From simple sites deployed via +copying files up to building custom themes/mu-plugins and installing npm/composer vendors - It handles all kinds of +WordPress installation types. ## Table Of Contents @@ -39,15 +41,18 @@ A collection of [Deployer](https://deployer.org) Tasks/Recipes to deploy WordPre ## Installation 1. Run `composer require gaambo/deployer-wordpress --dev` in your root directory -2. Choose one of the [recipes](#recipes) and copy the corresponding example files (`examples/base` or `examples/advanced`) into your root directory - **or** write your own. +2. Choose one of the [recipes](#recipes) and copy the corresponding example files (`examples/base` or + `examples/advanced`) into your root directory - **or** write your own. 3. Read through the recipe and customize it to your needs - here's a checklist: - - [ ] Check localhost configuration - - [ ] Set paths to your directory structure - - [ ] If you have a custom theme set it's name - if not remove the configuration and the theme build-tasks - - [ ] If you have a custom mu-plugin set it's name - if not remove the configuration and the mu-plugin build-tasks - - [ ] Check if the deployment flow meets your needs and maybe delete/add/overwrite tasks -4. Make your remote hosts ready for deployment (install composer, WP CLI; setup paths,...). Allthough the library checks for most of them and installs them. -5. Make a **test deployment** to a test/staging server. Do not directly deploy to your production site, you may break it. + - [ ] Check localhost configuration + - [ ] Set paths to your directory structure + - [ ] If you have a custom theme set it's name - if not remove the configuration and the theme build-tasks + - [ ] If you have a custom mu-plugin set it's name - if not remove the configuration and the mu-plugin build-tasks + - [ ] Check if the deployment flow meets your needs and maybe delete/add/overwrite tasks +4. Make your remote hosts ready for deployment (install composer, WP CLI; setup paths,...). Allthough the library checks + for most of them and installs them. +5. Make a **test deployment** to a test/staging server. Do not directly deploy to your production site, you may break + it. 6. Develop, deploy and be happy :) ## Requirements @@ -55,10 +60,12 @@ A collection of [Deployer](https://deployer.org) Tasks/Recipes to deploy WordPre Obviously: - [PHP](https://php.net/) and [composer](https://getcomposer.org) for installing and using Deployer -- [Deployer](https://deployer.org) core (`deployer/deployer`) is required dependencies of this package defined in `composer.json` +- [Deployer](https://deployer.org) core (`deployer/deployer`) is required dependencies of this package defined in + `composer.json` - WordPress installation + local web server and database to use it -Most of the tasks only run in *nix shells - so a *nix **operating system** is preferred. If you run Windows have a look at [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) to run Ubuntu Bash inside Windows. +Most of the tasks only run in *nix shells - so a *nix **operating system** is preferred. If you run Windows have a look +at [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) to run Ubuntu Bash inside Windows. Some tasks have additional requirements, eg: @@ -69,24 +76,34 @@ Some tasks have additional requirements, eg: ## Configuration -All tasks are documented and describe which options/variables need to be configured. `set.php` is included in all example recipes - This and the example recipes should have you covered regarding all required configurations. Further variables which need to be set by you are marked accordingly in the recipes. +All tasks are documented and describe which options/variables need to be configured. `set.php` is included in all +example recipes - This and the example recipes should have you covered regarding all required configurations. Further +variables which need to be set by you are marked accordingly in the recipes. To help understand all the configurations here are the thoughts behind theme: -The tasks are built to work with any kind of WordPress setup (vanilla, composer, subdirectory,..) - therefore all paths and directorys are configurable via variables. `set.php` contains some sane defaults which makes all tasks work out of the box with a default installation. +The tasks are built to work with any kind of WordPress setup (vanilla, composer, subdirectory,..) - therefore all paths +and directorys are configurable via variables. `set.php` contains some sane defaults which makes all tasks work out of +the box with a default installation. ### Default Directory Structure -My [Vanilla WordPress Boilerplate](https://github.com/gaambo/vanilla-wp/) uses this library. You can find a example configuration in the GitHub repository. +My [Vanilla WordPress Boilerplate](https://github.com/gaambo/vanilla-wp/) uses this library. You can find a example +configuration in the GitHub repository. ### wp-config.php -To make WordPress deployable you need to extract the host-dependent configuration (eg database access) into a seperate file which does not live in your git repository and is not deployed. I suggest using a **`wp-config-local.php`** file. This file should be required in your `wp-config.php` and be ignored by git (via `.gitignore`). This way `wp-config.php` can (should) be in your git repository and also be deployed. The default `wp/filter` configuration assumes this. +To make WordPress deployable you need to extract the host-dependent configuration (eg database access) into a seperate +file which does not live in your git repository and is not deployed. I suggest using a **`wp-config-local.php`** file. +This file should be required in your `wp-config.php` and be ignored by git (via `.gitignore`). This way `wp-config.php` +can (should) be in your git repository and also be deployed. The default `wp/filter` configuration assumes this. Another advantage of using a `wp-config-local.php` is to set `WP_DEBUG` on a per host basis. ### Rsync filters/excludes/includes The default rsync config for syncing files (used by all \*:push/\*:pull tasks) is set in the `rsync` variable. -By default it set's a `filter-perDir` argument as `.deployfilter` - which means rsync will look for a file named `.deployfilter` in each directory to parse filters for this directory. See [rsync man](https://linux.die.net/man/1/rsync) section "Filter Rules" for syntax. +By default it set's a `filter-perDir` argument as `.deployfilter` - which means rsync will look for a file named +`.deployfilter` in each directory to parse filters for this directory. +See [rsync man](https://linux.die.net/man/1/rsync) section "Filter Rules" for syntax. This can be handy to put int your custom theme or mu-plugin - for example: @@ -105,11 +122,13 @@ This can be handy to put int your custom theme or mu-plugin - for example: - package-lock.json ``` -This prevents any development files/development tools from syncing. I strongly recommend you put something like this in your custom theme and mu-plugins or overwrite any of the `themes/filter` or `mu-plugins/filter` configurations. +This prevents any development files/development tools from syncing. I strongly recommend you put something like this in +your custom theme and mu-plugins or overwrite any of the `themes/filter` or `mu-plugins/filter` configurations. ## Tasks -All tasks reside in the `src/tasks` directory and are documented well. Here's a summary of all tasks - for details (eg required variables/config) see their source. +All tasks reside in the `src/tasks` directory and are documented well. Here's a summary of all tasks - for details (eg +required variables/config) see their source. You can also run `dep list` to see all available tasks and their description. ### Database Tasks (`tasks/database.php`) @@ -123,12 +142,15 @@ You can also run `dep list` to see all available tasks and their description. ### File Tasks (`tasks/files.php`) -- `files:push`: Pushes all files from local to remote host (combines `wp:push`, `uploads:push`, `plugins:push`, `mu-plugins:push`, `themes:push`) -- `files:pull`: Pulls all files from remote to local host (combines `wp:pull`, `uploads:pull`, `plugins:pull`, `mu-plugins:pull`, `themes:pull`) +- `files:push`: Pushes all files from local to remote host (combines `wp:push`, `uploads:push`, `plugins:push`, + `mu-plugins:push`, `themes:push`) +- `files:pull`: Pulls all files from remote to local host (combines `wp:pull`, `uploads:pull`, `plugins:pull`, + `mu-plugins:pull`, `themes:pull`) ### Theme Tasks (`tasks/theme.php`) -> **Note:** It is recommended to use the new packages functionality for managing themes for better flexibility and control. +> **Note:** It is recommended to use the new packages functionality for managing themes for better flexibility and +> control. - `theme:assets:vendors`: Install theme assets vendors/dependencies (npm), can be run locally or remote - `theme:assets:build`: Run theme assets (npm) build script, can be run locally or remote @@ -151,7 +173,8 @@ You can also run `dep list` to see all available tasks and their description. ### Plugin Tasks (`tasks/plugins.php`) -> **Note:** It is recommended to use the new packages functionality for managing plugins for better flexibility and control. +> **Note:** It is recommended to use the new packages functionality for managing plugins for better flexibility and +> control. - `plugins:push`: Push plugins from local to remote - `plugins:pull`: Pull plugins from remote to local @@ -161,7 +184,8 @@ You can also run `dep list` to see all available tasks and their description. ### MU Plugin Tasks (`tasks/mu-plugins.php`) -> **Note:** It is recommended to use the new packages functionality for managing mu-plugins for better flexibility and control. +> **Note:** It is recommended to use the new packages functionality for managing mu-plugins for better flexibility and +> control. - `mu-plugin:vendors`: Install mu-plugin vendors (composer), can be run locally or remote - `mu-plugin`: A combined tasks to prepare the theme - at the moment only runs mu-plugin:vendors task @@ -177,22 +201,26 @@ You can also run `dep list` to see all available tasks and their description. - `wp:push`: Pushes WordPress core files via rsync - `wp:pull`: Pulls WordPress core files via rsync - `wp:info`: Runs the --info command via WP CLI - just a helper/test task -- `wp:install-wpcli`: Install the WP-CLI binary manually with the `wp:install-wpcli` task and set the path as `/bin/wp` afterwards. +- `wp:install-wpcli`: Install the WP-CLI binary manually with the `wp:install-wpcli` task and set the path as `/bin/wp` + afterwards. #### WP-CLI -Handling and installing the WP-CLI binary can be done in one of multiple ways: +Handling and installing the WP-CLI binary can be done in one of multiple ways: -1. The default `bin/wp` in `set.php` checks for a usable WP-CLI binary and if none is found it downloads and installs it to `{{deploy_path}}/.dep/wp-cli.phar` (this path is checked in the future as well). -2. If you want this behaviour (check if installed, else install) but in another path, overwrite the `bin/wp` variable with a function and change the path it should be installed to. +1. The default `bin/wp` in `set.php` checks for a usable WP-CLI binary and if none is found it downloads and installs it + to `{{deploy_path}}/.dep/wp-cli.phar` (this path is checked in the future as well). +2. If you want this behaviour (check if installed, else install) but in another path, overwrite the `bin/wp` variable + with a function and change the path it should be installed to. 3. Set the `bin/wp` variable path on the host configuration, if WP-CLI is already installed. 4. Install the WP-CLI binary manually with the `wp:install-wpcli` task and set the path as `/bin/wp` afterwards. -You can pass the installPath, binaryFile and sudo usage via CLI: -`dep wp:install-wpcli stage=production -o installPath='{{deploy_path}}/.bin -o binaryFile=wp -o sudo=true` + You can pass the installPath, binaryFile and sudo usage via CLI: + `dep wp:install-wpcli stage=production -o installPath='{{deploy_path}}/.bin -o binaryFile=wp -o sudo=true` See [original PR](https://github.com/gaambo/deployer-wordpress/pull/5) for more information. -There's a task for downloading core and `--info`. You can generate your own tasks to handle other WP-CLI commands, there's a util function `Gaambo\DeployerWordpress\Utils\WPCLI\runCommand` (`src/utils/wp-cli.php`); +There's a task for downloading core and `--info`. You can generate your own tasks to handle other WP-CLI commands, +there's a util function `Gaambo\DeployerWordpress\Utils\WPCLI\runCommand` (`src/utils/wp-cli.php`); ### Language Tasks (`tasks/languages.php`) @@ -205,30 +233,42 @@ There's a task for downloading core and `--info`. You can generate your own task ## Recipes Deployer WordPress ships with two base recipes which handle my use cases. -Both recipes are based on the default PHPDeployer common recipe and have their own recipe file which you can include in your `deploy.php` as a start. The examples folder provides examples for each recipe. +Both recipes are based on the default PHPDeployer common recipe and have their own recipe file which you can include in +your `deploy.php` as a start. The examples folder provides examples for each recipe. Both recipes log the deployed versions in PHPDeployers default format (`.dep` folder). -Both recipes overwrites the `deploy:update_code` Deployer task with a `deploy:update_code` task to deploy code via rsync instead of git +Both recipes overwrites the `deploy:update_code` Deployer task with a `deploy:update_code` task to deploy code via rsync +instead of git ### Base -This is for WordPress sites where you don't need symlinking per version or atomic releases. This means that on your remote/production host you just have on folder which contains all of WordPress files and this is served by your web server. -Since this is still based on the default PHPDeployer recipe which uses symlinking and to provide compatibility with all tasks, this just hardcodes the `release_path` and `current_path`. +This is for WordPress sites where you don't need symlinking per version or atomic releases. This means that on your +remote/production host you just have on folder which contains all of WordPress files and this is served by your web +server. +Since this is still based on the default PHPDeployer recipe which uses symlinking and to provide compatibility with all +tasks, this just hardcodes the `release_path` and `current_path`. ### Advanced -This uses symlinking like the default common recipe from PHPDeployer. Each release gets deployed in its own folder unter `{{deploy_path}}/releases/` and `{{deploy_path}}/current` is a symlink to the most current version. The symlink is automatically updated after the deployment finishes successfully. You can configure your webserver to just server `{{deploy_path}}/current`. +This uses symlinking like the default common recipe from PHPDeployer. Each release gets deployed in its own folder unter +`{{deploy_path}}/releases/` and `{{deploy_path}}/current` is a symlink to the most current version. The symlink is +automatically updated after the deployment finishes successfully. You can configure your webserver to just server +`{{deploy_path}}/current`. #### Custom Theme Set custom theme name (= directory) in variable `theme/name`. -By default it runs `theme:assets:vendors` and `theme:assets:build` locally and just pushes the built/dist files to the server (--> no need to install Node.js/npm on server). The `theme:assets` task (which groups the two tasks above) is hooked into _before_ `deploy:push_code`. +By default it runs `theme:assets:vendors` and `theme:assets:build` locally and just pushes the built/dist files to the +server (--> no need to install Node.js/npm on server). The `theme:assets` task (which groups the two tasks above) is +hooked into _before_ `deploy:push_code`. -Installing PHP/composer vendors/dependencies is done on the server. The `theme:vendors` task is therefore hooked into _after_ `deploy:push_code`. +Installing PHP/composer vendors/dependencies is done on the server. The `theme:vendors` task is therefore hooked into +_after_ `deploy:push_code`. #### Custom MU-Plugin Set custom mu-plugin name (=directory) in variable `mu-plugin/name`. -Installing PHP/composer vendors/dependencies is done on the server. The `mu-plugin:vendors` task is therefore hooked into _after_ `deploy:push_code`. +Installing PHP/composer vendors/dependencies is done on the server. The `mu-plugin:vendors` task is therefore hooked +into _after_ `deploy:push_code`. ## Changelog @@ -236,12 +276,14 @@ See [CHANGELOG.md](CHANGELOG.md). ## Contributing -If you have feature requests, find bugs or need help just open an issue on [GitHub](https://github.com/gaambo/deployer-wordpress). +If you have feature requests, find bugs or need help just open an issue +on [GitHub](https://github.com/gaambo/deployer-wordpress). Pull requests are always welcome. PSR2 coding standard are used and I try to adhere to Deployer best-practices. ### Testing -1. Download my [Vanilla WordPress Boilerplate](https://github.com/gaambo/vanilla-wp/) or set up a local dev environment with a deploy config +1. Download my [Vanilla WordPress Boilerplate](https://github.com/gaambo/vanilla-wp/) or set up a local dev environment + with a deploy config 2. Setup a remote test server 3. Configure yml/deploy.php 4. Run common tasks (`deploy`, `plugins:pull/push`, `db`) for both base as well as advanced recipe @@ -252,7 +294,8 @@ Pull requests are always welcome. PSR2 coding standard are used and I try to adh ## Packages (added in v@next) -The new packages system allows for more flexible handling of custom themes, plugins, and mu-plugins. Packages can be configured to manage assets, vendors, and deployment tasks. +The new packages system allows for more flexible handling of custom themes, plugins, and mu-plugins. Packages can be +configured to manage assets, vendors, and deployment tasks. ### Configuration @@ -292,4 +335,4 @@ set('packages', [ ### Best Practices - Ensure all package paths are correctly set relative to the `release_path` or `current_path`. -- Define npm build scripts in your `package.json` for asset management. \ No newline at end of file +- Define npm build scripts in your `package.json` for asset management. diff --git a/examples/bedrock/deploy.php b/examples/bedrock/deploy.php index 8151bdb..88a5ee5 100644 --- a/examples/bedrock/deploy.php +++ b/examples/bedrock/deploy.php @@ -22,7 +22,7 @@ ->set('project_path', __DIR__) ->set('current_path', __DIR__) // Bedrock dirs - ->set('uploads/path', '{{current_path}}') + ->set('uploads/path', '{{current_path}}') // Do not use shared directory for uploads. ->set('uploads/dir', 'web/app/uploads') ->set('mu-plugins/dir', 'web/app/mu-plugins') ->set('themes/dir', 'web/app/themes') diff --git a/examples/bedrock/deploy.yml b/examples/bedrock/deploy.yml index 9970955..7d574f7 100644 --- a/examples/bedrock/deploy.yml +++ b/examples/bedrock/deploy.yml @@ -7,7 +7,7 @@ hosts: hostname: test.dev public_url: https://test.dev deploy_path: "~" - release_path: "{{deploy_path}}/public_html" # fixed directory, no symlinks + current_path: "{{deploy_path}}/public_html" # fixed directory, no symlinks # Bedrock dirs uploads/dir: web/app/uploads mu-plugins/dir: web/app/mu-plugins diff --git a/examples/simple/deploy.yml b/examples/simple/deploy.yml index d7fb6ce..a10cd4e 100644 --- a/examples/simple/deploy.yml +++ b/examples/simple/deploy.yml @@ -9,6 +9,6 @@ hosts: hostname: test.dev public_url: https://test.dev deploy_path: "~" - release_path: "{{deploy_path}}/public_html" # fixed directory, no symlinks + current_path: "{{deploy_path}}/public_html" # fixed directory, no symlinks dbdump_path: ~/data/dumps backup_path: ~/data/backups diff --git a/recipes/bedrock.php b/recipes/bedrock.php index 6b7c01e..58ce979 100644 --- a/recipes/bedrock.php +++ b/recipes/bedrock.php @@ -13,11 +13,8 @@ use function Deployer\add; use function Deployer\after; -use function Deployer\get; use function Deployer\run; -use function Deployer\set; use function Deployer\task; -use function Deployer\test; use function Deployer\upload; require __DIR__ . '/common.php'; @@ -36,12 +33,14 @@ '.env.example', 'wp-cli.yml', ], "{{release_or_current_path}}", ['options' => $rsyncOptions]); + run("mkdir -p {{release_or_current_path}}/web"); upload([ // Keep prod .htaccess with webp redirects + redirection redirects + wprocket // 'web/.htaccess', 'web/index.php', 'web/wp-config.php', ], "{{release_or_current_path}}/web", ['options' => $rsyncOptions]); + run("mkdir -p {{release_or_current_path}}/{{mu-plugins/dir}}"); upload([ 'web/app/mu-plugins/bedrock-autoloader.php', ], "{{release_or_current_path}}/{{mu-plugins/dir}}", ['options' => $rsyncOptions]); diff --git a/recipes/common.php b/recipes/common.php index f520d4b..fc40f52 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -8,12 +8,14 @@ namespace Gaambo\DeployerWordpress\Recipes\Common; use Deployer\Deployer; +use Deployer\Exception\ConfigurationException; use Deployer\Host\Host; use Gaambo\DeployerWordpress\Composer; use Gaambo\DeployerWordpress\Localhost; use Gaambo\DeployerWordpress\WPCLI; use function Deployer\after; +use function Deployer\cd; use function Deployer\commandExist; use function Deployer\currentHost; use function Deployer\get; @@ -25,8 +27,10 @@ use function Deployer\run; use function Deployer\selectedHosts; use function Deployer\set; +use function Deployer\Support\escape_shell_argument; use function Deployer\task; use function Deployer\test; +use function Deployer\timestamp; use function Deployer\warning; use function Deployer\which; @@ -111,17 +115,20 @@ // PATHS & FILES CONFIGURATION -// Use fixed release_path always +// Use fixed current_path always - this will be available during deploys but also in standalone-tasks. set('release_or_current_path', function () { - return '{{release_path}}'; // Do not use get() to stay in same context. + return '{{current_path}}'; // Do not use get() to stay in same context. }); -// Use a dummy current_path because deployer checks if it's a symlink +/** + * Set the current_path to the "root" path of your project. All tasks and dirs will be relative to this. + */ set('current_path', function () { - if (test('[ ! -f {{deploy_path}}/.dep/current ]')) { - run('{{bin/symlink}} {{release_path}} {{deploy_path}}/.dep/current'); - } - return '{{deploy_path}}/.dep/current'; + throw new ConfigurationException('You should configure the current_path on the host and localhost.'); +}); + +set('release_path', function () { + throw new ConfigurationException('This recipe does not use (symlinked) releases. We only use current_path.'); }); // if you want to further define options for rsyncing files @@ -239,6 +246,43 @@ info("deploying to $hosts"); }); +// Overwrite deploy:setup to ignore existing current_path/release_path directories. This is expected. +task('deploy:setup', function () { + run( + << .dep/latest_release"); + + // Metainfo. + $timestamp = timestamp(); + $metainfo = [ + 'created_at' => $timestamp, + 'release_name' => $releaseName, + 'user' => get('user'), + 'target' => get('target'), + ]; + + // Save metainfo about release. + $json = escape_shell_argument(json_encode($metainfo)); + run("echo $json >> .dep/releases_log"); +}); + // Overwrite deploy:prepare to extract updating/pushing code to extra task task('deploy:prepare', [ 'deploy:info', diff --git a/src/Files.php b/src/Files.php index ab459ac..8d3a865 100644 --- a/src/Files.php +++ b/src/Files.php @@ -4,6 +4,7 @@ use function Deployer\download; use function Deployer\run; +use function Deployer\runLocally; use function Deployer\upload; /** @@ -38,7 +39,7 @@ public static function pushFiles(string $localPath, string $remotePath, array $r public static function pullFiles(string $remotePath, string $localPath, array $rsyncOptions = []): void { $localPath = Localhost::getConfig('current_path') . '/' . $localPath; - Localhost::run("mkdir -p $localPath"); // Always ensure directory exists. + runLocally("mkdir -p $localPath"); // Always ensure directory exists. download('{{release_or_current_path}}/' . $remotePath . '/', $localPath . '/', ['options' => $rsyncOptions]); } diff --git a/src/Localhost.php b/src/Localhost.php index 66df9d8..8d5f90b 100644 --- a/src/Localhost.php +++ b/src/Localhost.php @@ -7,6 +7,7 @@ use Deployer\Host\Host; use Deployer\Task\Context; +use function Deployer\on; use function Deployer\runLocally; /** @@ -47,16 +48,10 @@ public static function get(): Host */ public static function run(string $command, ?array $options = []): string { - // Let's build a configuration object that has all the localhost values - // But has the current context and global deployer context as parents to fallback. - $config = new Configuration(); - $config->update(self::get()->config()->ownValues()); - if (Context::has()) { - $config->bind(Context::get()->getConfig()); - } else { - $config->bind(Deployer::get()->config); - } - $command = $config->parse($command); - return runLocally($command, $options); + $result = null; + on(self::get(), function () use ($command, $options, &$result) { + $result = runLocally($command, $options); + }); + return $result; } } diff --git a/src/NPM.php b/src/NPM.php index d43c3e1..f7fce45 100644 --- a/src/NPM.php +++ b/src/NPM.php @@ -48,8 +48,7 @@ public static function runCommand(string $path, string $action, string $argument } /** - * Run npm install - * Tries to copy node_modules from previous release if available + * Run npm install. * * @param string $path Path in which to run npm install * @param string $arguments Command-line arguments to be passed to npm @@ -57,11 +56,6 @@ public static function runCommand(string $path, string $action, string $argument */ public static function runInstall(string $path, string $arguments = ''): string { - if (has('previous_release')) { - if (test('[ -d {{previous_release}}/node_modules ]')) { - run("cp -R {{previous_release}}/node_modules $path"); - } - } return self::runCommand($path, 'install', $arguments); } } diff --git a/tasks/database.php b/tasks/database.php index d90ded8..0f26852 100644 --- a/tasks/database.php +++ b/tasks/database.php @@ -40,13 +40,14 @@ $localDumpPath = Localhost::getConfig('dbdump_path'); $remoteDumpPath = get('dbdump_path'); $now = date('Y-m-d_H-i', time()); - set('dbdump/file', "db_backup-$now.sql"); + $dumpFile = "db_backup-$now.sql"; + set('dbdump/file', $dumpFile); run('mkdir -p ' . get('dbdump_path')); - WPCLI::runCommand("db export $remoteDumpPath/{{dbdump/file}} --add-drop-table", "{{release_or_current_path}}"); + WPCLI::runCommand("db export $remoteDumpPath/$dumpFile --add-drop-table", "{{release_or_current_path}}"); Localhost::run("mkdir -p $localDumpPath"); - download("$remoteDumpPath/{{dbdump/file}}", "$localDumpPath/{{dbdump/file}}"); + download("$remoteDumpPath/$dumpFile", "$localDumpPath/$dumpFile"); })->desc('Create backup of remote database and download locally'); /** @@ -63,15 +64,16 @@ $localDumpPath = Localhost::getConfig('dbdump_path'); $remoteDumpPath = get('dbdump_path'); $now = date('Y-m-d_H-i', time()); - set('dbdump/file', "db_backup-$now.sql"); + $dumpFile = "db_backup-$now.sql"; + set('dbdump/file', $dumpFile); Localhost::run("mkdir -p $localDumpPath"); - WPCLI::runCommandLocally("db export $localDumpPath/{{dbdump/file}} --add-drop-table"); + WPCLI::runCommandLocally("db export $localDumpPath/$dumpFile --add-drop-table"); run('mkdir -p {{dbdump_path}}'); upload( - "$localDumpPath/{{dbdump/file}}", - "$remoteDumpPath/{{dbdump/file}}" + "$localDumpPath/$dumpFile", + "$remoteDumpPath/$dumpFile" ); })->desc('Create backup of local database and upload to remote'); diff --git a/tasks/packages.php b/tasks/packages.php index 085be0e..3b3503d 100644 --- a/tasks/packages.php +++ b/tasks/packages.php @@ -130,7 +130,6 @@ $rsyncOptions = Rsync::buildOptionsArray([ 'filter' => $package['rsync:filter'] ?? [], ]); - run("mkdir -p {{release_or_current_path}}/$remotePath"); Files::pushFiles($packagePath, $remotePath, $rsyncOptions); } })->desc('Push packages from local to remote'); diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php index 95c668b..bfcd99a 100644 --- a/tests/Functional/FunctionalTestCase.php +++ b/tests/Functional/FunctionalTestCase.php @@ -108,7 +108,7 @@ private function setUpHosts(): void // Remote host setup (using Localhost for testing so rsync runs on the same host) $this->remoteHost = new Localhost('testremote'); $this->remoteHost->set('deploy_path', $this->remoteDir); - $this->remoteHost->set('release_path', $this->remoteReleaseDir); + $this->remoteHost->set('current_path', $this->remoteReleaseDir); $this->remoteHost->set('dbdump_path', $this->remoteDir . '/data/db_dumps'); $this->remoteHost->set('backup_path', $this->remoteDir . '/data/backups'); $this->remoteHost->set('bin/wp', 'wp'); diff --git a/tests/Functional/Tasks/LanguageTasksFunctionalTest.php b/tests/Functional/Tasks/LanguageTasksFunctionalTest.php index be6f320..15f19c3 100644 --- a/tests/Functional/Tasks/LanguageTasksFunctionalTest.php +++ b/tests/Functional/Tasks/LanguageTasksFunctionalTest.php @@ -42,7 +42,6 @@ public function testListAvailableTasks(): void $this->assertStringContainsString('languages:push', $output); $this->assertStringContainsString('languages:pull', $output); - $this->assertStringContainsString('languages:sync', $output); $this->assertStringContainsString('languages:backup:remote', $output); $this->assertStringContainsString('languages:backup:local', $output); } diff --git a/tests/Integration/NPMIntegrationTest.php b/tests/Integration/NPMIntegrationTest.php index f065c1e..3bd2171 100644 --- a/tests/Integration/NPMIntegrationTest.php +++ b/tests/Integration/NPMIntegrationTest.php @@ -86,80 +86,6 @@ public function testRunInstall(): void $this->addToAssertionCount(1); // Count the mock expectation as an assertion } - public function testRunInstallWithPreviousRelease(): void - { - $path = '/var/www/html'; - - // Set previous_release config - $this->deployer->config->set('previous_release', '/var/www/releases/1'); - - // ProcessRunner will be called 3 times: - // 1. test command to check if node_modules exists (wrapped in bash-if) - // 2. cp command to copy node_modules - // 3. npm install command - $this->processRunnerMock - ->expects($this->exactly(3)) - ->method('run') - ->willReturnCallback(function ($host, $command) use ($path) { - static $callNumber = 0; - $callNumber++; - - switch ($callNumber) { - case 1: - // test() wraps the command in a bash-if and checks for success via echo - $this->assertStringContainsString('if [ -d /var/www/releases/1/node_modules ]; then echo +', $command); - // Extract the random value from the command string - if (preg_match('/echo \+([a-z]+);/', $command, $matches)) { - return '+' . $matches[1]; - } - throw new \RuntimeException('Could not extract random value from command'); - case 2: - $this->assertEquals("cp -R /var/www/releases/1/node_modules $path", $command); - return 'Copy output'; - case 3: - $this->assertEquals("cd $path && npm install", $command); - return 'NPM output'; - } - }); - - $result = NPM::runInstall($path); - $this->assertEquals('NPM output', $result); - $this->addToAssertionCount(3); // Count the mock callback assertions - } - - public function testRunInstallWithPreviousReleaseNoNodeModules(): void - { - $path = '/var/www/html'; - - // Set previous_release config - $this->deployer->config->set('previous_release', '/var/www/releases/1'); - - // ProcessRunner will be called 2 times: - // 1. test command to check if node_modules exists (wrapped in bash-if, returns empty) - // 2. npm install command - $this->processRunnerMock - ->expects($this->exactly(2)) - ->method('run') - ->willReturnCallback(function ($host, $command) use ($path) { - static $callNumber = 0; - $callNumber++; - - switch ($callNumber) { - case 1: - // test() wraps the command in a bash-if and checks for success via echo - $this->assertStringContainsString('if [ -d /var/www/releases/1/node_modules ]; then echo +', $command); - return '0'; // False value. - case 2: - $this->assertEquals("cd $path && npm install", $command); - return 'NPM output'; - } - }); - - $result = NPM::runInstall($path); - $this->assertEquals('NPM output', $result); - $this->addToAssertionCount(2); // Count the mock callback assertions - } - /** * @dataProvider verbosityProvider */ @@ -284,41 +210,6 @@ public function testRunCommandWithCustomNpmBinary(): void $this->assertEquals('NPM output', $result); } - public function testRunInstallWithCustomPreviousReleasePath(): void - { - $path = '/var/www/html'; - $customReleasePath = '/var/www/custom/releases/2'; - - // Set custom previous_release path - $this->deployer->config->set('previous_release', $customReleasePath); - - $this->processRunnerMock - ->expects($this->exactly(3)) - ->method('run') - ->willReturnCallback(function ($host, $command) use ($path, $customReleasePath) { - static $callNumber = 0; - $callNumber++; - - switch ($callNumber) { - case 1: - $this->assertStringContainsString("if [ -d $customReleasePath/node_modules ]; then echo +", $command); - if (preg_match('/echo \+([a-z]+);/', $command, $matches)) { - return '+' . $matches[1]; - } - throw new \RuntimeException('Could not extract random value from command'); - case 2: - $this->assertEquals("cp -R $customReleasePath/node_modules $path", $command); - return 'Copy output'; - case 3: - $this->assertEquals("cd $path && npm install", $command); - return 'NPM output'; - } - }); - - $result = NPM::runInstall($path); - $this->assertEquals('NPM output', $result); - } - public function testRunCommandWithInvalidNpmBinary(): void { $path = '/var/www/html'; @@ -333,17 +224,4 @@ public function testRunCommandWithInvalidNpmBinary(): void $this->expectExceptionMessage('Config option "bin/npm" does not exist'); $result = NPM::runCommand($path, $action, $arguments); } - - public function testRunInstallWithInvalidPreviousReleasePath(): void - { - $path = '/var/www/html'; - - // Set invalid previous_release path (should skip copying) - $this->deployer->config->set('previous_release', null); - - $this->expectException(ConfigurationException::class); - $this->expectExceptionMessage('Config option "previous_release" does not exist'); - - $result = NPM::runInstall($path); - } -} \ No newline at end of file +} From 2abed89327ee7aa2ae634413c6ab79f2f852f114 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Thu, 17 Apr 2025 14:34:08 +0200 Subject: [PATCH 30/36] fix recipe include path when installed in projects --- recipes/common.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/common.php b/recipes/common.php index fc40f52..9b14f14 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -38,7 +38,7 @@ $commonRecipePaths = [ __DIR__ . '/../vendor/deployer/deployer/recipe/common.php', // Development/testing - __DIR__ . '/../deployer/deployer/recipe/common.php' // Installed via composer + __DIR__ . '/../../../deployer/deployer/recipe/common.php' // Installed via composer ]; foreach ($commonRecipePaths as $recipePath) { From 912213987301e5e287cc8475547af8fbcfe8d555 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Fri, 9 May 2025 09:51:20 +0200 Subject: [PATCH 31/36] fix db pulling and local importing --- tasks/database.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tasks/database.php b/tasks/database.php index 0f26852..09cbe60 100644 --- a/tasks/database.php +++ b/tasks/database.php @@ -123,21 +123,22 @@ task('db:local:import', function () { // Check if dump file exists $localDumpPath = Localhost::getConfig('dbdump_path'); - if (!has('dbdump/file') || !testLocally("[ -f $localDumpPath/{{dbdump/file}} ]")) { - throw new \RuntimeException("Database dump file not found at $localDumpPath/{{dbdump/file}}"); + $dumpFile = get('dbdump/file'); + if (!has('dbdump/file') || !testLocally("[ -f $localDumpPath/$dumpFile ]")) { + throw new \RuntimeException("Database dump file not found at $localDumpPath/$dumpFile"); } - $localUrl = Localhost::getConfig('public_url'); - WPCLI::runCommandLocally("db import $localDumpPath/{{dbdump/file}}"); - WPCLI::runCommandLocally("search-replace {{public_url}} $localUrl"); + $remoteUrl = get('public_url'); + WPCLI::runCommandLocally("db import $localDumpPath/$dumpFile"); + WPCLI::runCommandLocally("search-replace $remoteUrl {{public_url}}"); // If the local uploads directory is different from the remote one // replace all references to the remotes uploads directory with the local one - $localUploadsDir = Localhost::getConfig('uploads/dir'); - if ($localUploadsDir !== get('uploads/dir')) { - WPCLI::runCommandLocally("search-replace {{uploads/dir}} $localUploadsDir"); + $remoteUploadsDir = get('uploads/dir'); + if ($remoteUploadsDir !== Localhost::getConfig('uploads/dir')) { + WPCLI::runCommandLocally("search-replace $remoteUploadsDir {{uploads/dir}}"); } - Localhost::run("rm -f $localDumpPath/{{dbdump/file}}"); + Localhost::run("rm -f $localDumpPath/$dumpFile"); })->desc('Import database backup on local host'); /** From c177590a7774a584f349bf37640a6b9d04565dfd Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Thu, 3 Jul 2025 10:16:01 +0200 Subject: [PATCH 32/36] localhost: use context for getting config values --- src/Localhost.php | 8 +++-- .../Integration/LocalhostIntegrationTest.php | 36 ++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Localhost.php b/src/Localhost.php index 8d5f90b..e06383b 100644 --- a/src/Localhost.php +++ b/src/Localhost.php @@ -2,7 +2,6 @@ namespace Gaambo\DeployerWordpress; -use Deployer\Configuration\Configuration; use Deployer\Deployer; use Deployer\Host\Host; use Deployer\Task\Context; @@ -23,7 +22,12 @@ class Localhost */ public static function getConfig(string $key): mixed { - return self::get()->get($key); + // Switch to the localhost config, so all get() calls use that context. + // Useful for dynamic calculated values. + Context::push(new Context(self::get())); + $value = self::get()->get($key); + Context::pop(); + return $value; } /** diff --git a/tests/Integration/LocalhostIntegrationTest.php b/tests/Integration/LocalhostIntegrationTest.php index cc1158f..fb44007 100644 --- a/tests/Integration/LocalhostIntegrationTest.php +++ b/tests/Integration/LocalhostIntegrationTest.php @@ -2,8 +2,8 @@ namespace Gaambo\DeployerWordpress\Tests\Integration; +use Deployer\Task\Context; use Gaambo\DeployerWordpress\Localhost; -use PHPUnit\Framework\MockObject\MockObject; class LocalhostIntegrationTest extends IntegrationTestCase { @@ -24,17 +24,45 @@ public function testGetConfigWithNonExistentKey(): void $this->assertNull($value); } + public function testGetConfigWithDynamicValue(): void + { + $this->host->set('test_key', function () { + return 'test_value'; + }); + // Test getting configuration + $value = Localhost::getConfig('test_key'); + $this->assertEquals('test_value', $value); + } + + public function testCorrectlyUsesLocalhostContextWithDynamicValues(): void + { + $this->host->set('test_key', 'test_value'); + + $this->host->set('dynamic_key', function () { + return \Deployer\get('test_key'); + }); + $remoteHost = new \Deployer\Host\Localhost('testremote'); + $remoteHost->set('test_key', 'remote_value'); + // Push a new context with the remote host + Context::push(new Context($remoteHost)); + + $value = Localhost::getConfig('dynamic_key'); + $this->assertEquals('test_value', $value); + + Context::pop(); + } + public function testGet(): void { // Test getting localhost instance $host = Localhost::get(); - + // Verify it's the same instance we set up in IntegrationTestCase $this->assertSame($this->host, $host); - + // Verify it has the expected configuration $this->assertEquals('/var/www', $host->get('deploy_path')); $this->assertEquals('wp', $host->get('bin/wp')); $this->assertEquals('/var/www/current', $host->get('release_or_current_path')); } -} \ No newline at end of file +} From 584b70211dfed26a2c0781efea8d3ec8de361f6f Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Thu, 3 Jul 2025 10:39:52 +0200 Subject: [PATCH 33/36] add multisite flag and enable url replacements for multisites --- .gitignore | 1 + examples/multisite/deploy.php | 51 ++++++++ examples/multisite/deploy.yml | 12 ++ examples/simple/deploy.yml | 2 - recipes/common.php | 15 +++ tasks/database.php | 24 +++- tests/Functional/FunctionalTestCase.php | 12 +- .../Tasks/DatabaseTasksFunctionalTest.php | 123 ++++++++++++++++++ 8 files changed, 231 insertions(+), 9 deletions(-) create mode 100644 examples/multisite/deploy.php create mode 100644 examples/multisite/deploy.yml diff --git a/.gitignore b/.gitignore index 7042aef..ed054e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /vendor /.cursor +/.junie /.idea .DS_STORE .phpunit.cache diff --git a/examples/multisite/deploy.php b/examples/multisite/deploy.php new file mode 100644 index 0000000..c39e497 --- /dev/null +++ b/examples/multisite/deploy.php @@ -0,0 +1,51 @@ +set('public_url', "{{local_url}}") + ->set('project_path', __DIR__) + ->set('current_path', 'public') // The public doc root as kind of the currents release path. + ->set('dbdump_path', __DIR__ . '/data/db_dumps') + ->set('backup_path', __DIR__ . '/data/backups'); + +set('wp/multisite', true); + +set('packages', [ + 'theme' => [ + 'path' => '{{themes/dir}}/custom-theme', + 'remote:path' => '{{themes/dir}}/custom-theme', + 'assets' => true, + 'assets:build_script' => 'build' + ], + 'core-functionality' => [ + 'path' => '{{mu-plugins/dir}}/core-functionality', + 'remote:path' => '{{mu-plugins/dir}}/core-functionality' + ], +]); + +// Build package assets via npm locally +task('deploy:build_assets', function () { + on(Localhost::get(), function () { + if (has('packages')) { + // Do not install vendors on each deployment. + // invoke('packages:assets:vendors'); + invoke('packages:assets:build'); + } + }); +})->once(); diff --git a/examples/multisite/deploy.yml b/examples/multisite/deploy.yml new file mode 100644 index 0000000..4d57012 --- /dev/null +++ b/examples/multisite/deploy.yml @@ -0,0 +1,12 @@ +config: + local_url: http://test.local +hosts: + prod: + labels: + stage: production + hostname: test.dev + public_url: https://test.dev + deploy_path: "~" + current_path: "{{deploy_path}}/public_html" # fixed directory, no symlinks + dbdump_path: ~/data/dumps + backup_path: ~/data/backups diff --git a/examples/simple/deploy.yml b/examples/simple/deploy.yml index a10cd4e..4d57012 100644 --- a/examples/simple/deploy.yml +++ b/examples/simple/deploy.yml @@ -1,6 +1,4 @@ config: - theme/name: custom-theme - mu-plugin/name: core-functionality local_url: http://test.local hosts: prod: diff --git a/recipes/common.php b/recipes/common.php index 9b14f14..06b320e 100644 --- a/recipes/common.php +++ b/recipes/common.php @@ -131,6 +131,18 @@ throw new ConfigurationException('This recipe does not use (symlinked) releases. We only use current_path.'); }); +/** + * A helper variable to get the host name from the url + */ +set('public_host', function () { + $url = get('public_url'); + $host = parse_url($url, PHP_URL_HOST); + if (!$host) { + throw new ConfigurationException('Public url does not seem to be a valid url. Cannot parse host from the url.'); + } + return $host; +}); + // if you want to further define options for rsyncing files // just look at the source in `Files.php` and `Rsync.php` // and use the Rsync::buildOptionsArray and Files::push/pull methods @@ -185,6 +197,9 @@ set('languages/dir', 'wp-content/languages'); // relative to document root set('languages/filter', []); // rsync filter syntax +// Whether the site is a multisite and database imports/URL replacements should take that into account. +set('wp/multisite', false); + // options for zipping files for backups - passed to zip shell command set('zip_options', '-x "_backup_*.zip" -x **/node_modules/**\* -x **/vendor/**\*'); diff --git a/tasks/database.php b/tasks/database.php index 09cbe60..29d78c2 100644 --- a/tasks/database.php +++ b/tasks/database.php @@ -95,8 +95,18 @@ } $localUrl = Localhost::getConfig('public_url'); + $remoteUrl = get('public_url'); WPCLI::runCommand("db import {{dbdump_path}}/{{dbdump/file}}"); - WPCLI::runCommand("search-replace $localUrl {{public_url}}"); + + if (get('wp/multisite')) { + WPCLI::runCommand("search-replace $localUrl $remoteUrl --network --all-tables"); + + $localUrlHost = Localhost::getConfig('public_host'); + $publicUrlHost = get('public_host'); + WPCLI::runCommand("search-replace $localUrlHost $publicUrlHost --network --all-tables"); + } else { + WPCLI::runCommand("search-replace $localUrl $remoteUrl"); + } // If the local uploads directory is different from the remote one // replace all references to the local uploads directory with the remote one @@ -129,7 +139,17 @@ } $remoteUrl = get('public_url'); WPCLI::runCommandLocally("db import $localDumpPath/$dumpFile"); - WPCLI::runCommandLocally("search-replace $remoteUrl {{public_url}}"); + + if (get('wp/multisite')) { + $localUrl = Localhost::getConfig('public_url'); + WPCLI::runCommandLocally("search-replace $remoteUrl $localUrl --network --all-tables"); + + $localUrlHost = Localhost::getConfig('public_host'); + $publicUrlHost = get('public_host'); + WPCLI::runCommandLocally("search-replace $publicUrlHost $localUrlHost --network --all-tables"); + } else { + WPCLI::runCommandLocally("search-replace $remoteUrl {{public_url}}"); + } // If the local uploads directory is different from the remote one // replace all references to the remotes uploads directory with the local one diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php index bfcd99a..1e1bb4a 100644 --- a/tests/Functional/FunctionalTestCase.php +++ b/tests/Functional/FunctionalTestCase.php @@ -140,16 +140,18 @@ protected function setUpMockedServices(): void * * @param array $commandsToMock Map of command patterns to callables or return values */ - protected function mockCommands(array $commandsToMock): void + protected function mockCommands(array $commandsToMock, ?string $hostAlias = null): void { // Set up the mock to handle specific commands $this->mockedRunner->expects($this->any()) ->method('run') - ->willReturnCallback(function ($host, $command, $options = []) use ($commandsToMock) { + ->willReturnCallback(function ($host, $command, $options = []) use ($commandsToMock, $hostAlias) { // Check if this command should be mocked - foreach ($commandsToMock as $pattern => $handler) { - if (str_contains($command, $pattern)) { - return is_callable($handler) ? $handler($host, $command, $options) : $handler; + if (!$hostAlias || $host->getAlias() === $hostAlias) { + foreach ($commandsToMock as $pattern => $handler) { + if (str_contains($command, $pattern)) { + return is_callable($handler) ? $handler($host, $command, $options) : $handler; + } } } diff --git a/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php b/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php index 32d91a9..ad770b9 100644 --- a/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php +++ b/tests/Functional/Tasks/DatabaseTasksFunctionalTest.php @@ -601,6 +601,129 @@ public function testDbLocalImportWithMissingDumpFile(): void $this->assertNotEquals(0, $result, 'Task should fail when dump file is missing'); } + /** + * Helper method to mock successful multisite URL replacements + */ + protected function mockSuccessfulMultisiteDbImport(string $hostName): void + { + $this->mockCommands([ + 'wp db import' => function () { + return 'Database imported successfully'; + }, + 'wp search-replace' => function ($host, $command) { + if (strpos($command, '--network --all-tables') !== false) { + return 'Made some replacements across all network tables'; + } + return 'Made some replacements'; + } + ], $hostName); + } + + public function testDbLocalImportHandleMultisite(): void + { + // Enable multisite + $this->remoteHost->set('wp/multisite', true); + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->localDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock successful multisite URL replacements + $this->mockSuccessfulMultisiteDbImport('localhost'); + + // Run the multisite import task + $result = $this->dep('db:local:import'); + $output = $this->tester->getDisplay(); + $this->assertEquals(0, $result, 'Task should succeed with valid configuration'); + } + + public function testDbLocalImportHandleMultisiteWithUrlReplaceError(): void + { + // Enable multisite + $this->remoteHost->set('wp/multisite', true); + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->localDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock failed URL replacement + $this->mockCommands([ + 'wp db import' => function () { + return 'Database imported successfully'; + }, + 'wp search-replace' => function ($host, $command) { + if (strpos($command, '--network --all-tables') !== false) { + throw new RuntimeException('Multisite URL replacement failed'); + } + return 'Made some replacements'; + } + ], 'localhost'); + + // Run the multisite import task and expect failure + $result = $this->dep('db:local:import'); + $this->assertNotEquals(0, $result, 'Task should fail when URL replacement fails'); + } + + public function testDbRemoteImportHandleMultisite(): void + { + // Enable multisite + $this->remoteHost->set('wp/multisite', true); + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->remoteDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock successful multisite URL replacements + $this->mockSuccessfulMultisiteDbImport('testremote'); + + // Run the multisite import task + $result = $this->dep('db:remote:import'); + $this->assertEquals(0, $result, 'Task should succeed with valid configuration'); + } + + public function testDbRemoteImportHandleMultisiteWithUrlReplaceError(): void + { + // Enable multisite + $this->remoteHost->set('wp/multisite', true); + // Set up URLs for replacement + $this->localHost->set('public_url', 'http://localhost'); + $this->remoteHost->set('public_url', 'https://example.com'); + + // Create a dump file to import + $dumpFile = $this->remoteDir . '/dumps/db_backup.sql'; + copy($this->getFixturePath('database/dump.sql'), $dumpFile); + set('dbdump/file', 'db_backup.sql'); + + // Mock failed URL replacement + $this->mockCommands([ + 'wp db import' => function () { + return 'Database imported successfully'; + }, + 'wp search-replace' => function ($host, $command) { + if (strpos($command, '--network --all-tables') !== false) { + throw new RuntimeException('Multisite URL replacement failed'); + } + return 'Made some replacements'; + } + ], 'testremote'); + + // Run the multisite import task and expect failure + $result = $this->dep('db:remote:import'); + $this->assertNotEquals(0, $result, 'Task should fail when URL replacement fails'); + } + protected function setUp(): void { parent::setUp(); From 8f7c278816275c7fec51e6bcd151f8ec2db02c04 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Wed, 24 Sep 2025 10:48:51 +0200 Subject: [PATCH 34/36] add 'vendors' flag to packages to only run composer install if enabled --- README.md | 16 +- examples/bedrock/deploy.php | 2 +- examples/multisite/deploy.php | 3 +- examples/simple/deploy.php | 3 +- recipes/simple.php | 7 +- tasks/packages.php | 4 + .../Tasks/PackagesTasksFunctionalTest.php | 181 ++++++++++++++++++ 7 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 tests/Functional/Tasks/PackagesTasksFunctionalTest.php diff --git a/README.md b/README.md index 765724b..79e00b0 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,13 @@ WordPress installation types. 2. Choose one of the [recipes](#recipes) and copy the corresponding example files (`examples/base` or `examples/advanced`) into your root directory - **or** write your own. 3. Read through the recipe and customize it to your needs - here's a checklist: - - [ ] Check localhost configuration - - [ ] Set paths to your directory structure - - [ ] If you have a custom theme set it's name - if not remove the configuration and the theme build-tasks - - [ ] If you have a custom mu-plugin set it's name - if not remove the configuration and the mu-plugin build-tasks - - [ ] Check if the deployment flow meets your needs and maybe delete/add/overwrite tasks + +- [ ] Check localhost configuration +- [ ] Set paths to your directory structure +- [ ] If you have a custom theme set it's name - if not remove the configuration and the theme build-tasks +- [ ] If you have a custom mu-plugin set it's name - if not remove the configuration and the mu-plugin build-tasks +- [ ] Check if the deployment flow meets your needs and maybe delete/add/overwrite tasks + 4. Make your remote hosts ready for deployment (install composer, WP CLI; setup paths,...). Allthough the library checks for most of them and installs them. 5. Make a **test deployment** to a test/staging server. Do not directly deploy to your production site, you may break @@ -308,6 +310,7 @@ set('packages', [ 'remote:path' => 'path/on/remote', 'assets' => true, 'assets:build_script' => 'build', + 'vendors' => true, ], // Add more packages as needed ]); @@ -327,7 +330,8 @@ set('packages', [ ], 'core-functionality' => [ 'path' => '{{mu-plugins/dir}}/core-functionality', - 'remote:path' => '{{mu-plugins/dir}}/core-functionality' + 'remote:path' => '{{mu-plugins/dir}}/core-functionality', + 'vendors' => true, ], ]); ``` diff --git a/examples/bedrock/deploy.php b/examples/bedrock/deploy.php index 88a5ee5..a7a6226 100644 --- a/examples/bedrock/deploy.php +++ b/examples/bedrock/deploy.php @@ -40,7 +40,7 @@ ], 'core-functionality' => [ 'path' => '{{mu-plugins/dir}}/core-functionality', - 'remote:path' => '{{mu-plugins/dir}}/core-functionality' + 'remote:path' => '{{mu-plugins/dir}}/core-functionality', ], ]); diff --git a/examples/multisite/deploy.php b/examples/multisite/deploy.php index c39e497..561b75d 100644 --- a/examples/multisite/deploy.php +++ b/examples/multisite/deploy.php @@ -35,7 +35,8 @@ ], 'core-functionality' => [ 'path' => '{{mu-plugins/dir}}/core-functionality', - 'remote:path' => '{{mu-plugins/dir}}/core-functionality' + 'remote:path' => '{{mu-plugins/dir}}/core-functionality', + 'vendors' => true, ], ]); diff --git a/examples/simple/deploy.php b/examples/simple/deploy.php index 8ea5dcf..e7cb57d 100644 --- a/examples/simple/deploy.php +++ b/examples/simple/deploy.php @@ -33,7 +33,8 @@ ], 'core-functionality' => [ 'path' => '{{mu-plugins/dir}}/core-functionality', - 'remote:path' => '{{mu-plugins/dir}}/core-functionality' + 'remote:path' => '{{mu-plugins/dir}}/core-functionality', + 'vendors' => true, ], ]); diff --git a/recipes/simple.php b/recipes/simple.php index 9e5cf4a..ae582c5 100644 --- a/recipes/simple.php +++ b/recipes/simple.php @@ -9,11 +9,8 @@ namespace Gaambo\DeployerWordpress\Recipes\Simple; use function Deployer\add; -use function Deployer\get; -use function Deployer\run; -use function Deployer\set; +use function Deployer\after; use function Deployer\task; -use function Deployer\test; require __DIR__ . '/common.php'; @@ -22,6 +19,8 @@ task('deploy:update_code', ['packages:push']) ->desc('Pushes local packages to the remote hosts'); +after('packages:push', 'packages:vendors'); + task('deploy', [ 'deploy:prepare', 'deploy:build_assets', diff --git a/tasks/packages.php b/tasks/packages.php index 3b3503d..f120fb9 100644 --- a/tasks/packages.php +++ b/tasks/packages.php @@ -87,6 +87,7 @@ * * Configuration per package: * - path: Path of package relative to release_path/current_path + * - vendors: Whether the package has composer vendors to install * * Example: * dep packages:vendors prod @@ -94,6 +95,9 @@ task('packages:vendors', function () { foreach (get('packages', []) as $package) { $packagePath = $package['path']; + if (empty($package['vendors'])) { + continue; + } Composer::runDefault( "{{release_or_current_path}}/$packagePath" ); diff --git a/tests/Functional/Tasks/PackagesTasksFunctionalTest.php b/tests/Functional/Tasks/PackagesTasksFunctionalTest.php new file mode 100644 index 0000000..f1b0789 --- /dev/null +++ b/tests/Functional/Tasks/PackagesTasksFunctionalTest.php @@ -0,0 +1,181 @@ +localMuPluginsDir = $this->localDocRootDir . '/wp-content/mu-plugins'; + $this->remoteMuPluginsDir = $this->remoteReleaseDir . '/wp-content/mu-plugins'; + + // Create directories + mkdir($this->localMuPluginsDir, 0755, true); + mkdir($this->remoteMuPluginsDir, 0755, true); + } + + public function testListAvailableTasks(): void + { + $this->dep('list', null); + $output = $this->tester->getDisplay(); + + $this->assertStringContainsString('packages:assets:vendors', $output); + $this->assertStringContainsString('packages:assets:build', $output); + $this->assertStringContainsString('packages:vendors', $output); + $this->assertStringContainsString('packages:assets', $output); + $this->assertStringContainsString('packages', $output); + } + + public function testPackagesAssetsVendorsOnlyRunsWhenAssetsIsTrue(): void + { + // Copy test plugin from fixtures to local + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin-with-assets'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin-without-assets'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin-without-assets-flag'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Create test packages configuration + $this->deployer->config->set('packages', [ + [ + 'path' => '{{mu-plugins/dir}}/test-plugin-with-assets', + 'assets' => true + ], + [ + 'path' => '{{mu-plugins/dir}}/test-plugin-without-assets', + 'assets' => false + ], + [ + 'path' => '{{mu-plugins/dir}}/test-plugin-without-assets-flag' + // No assets flag + ] + ]); + + // Mock npm install command + $this->mockCommands([ + 'npm install' => function ($host, $command) { + if(str_contains($command, 'test-plugin-without-assets') || str_contains($command, 'test-plugin-without-assets-flag')) { + throw new RuntimeException('npm install called for plugin without assets/package.json'); + } + return 'npm install completed.'; + }, + ], 'testremote'); + + // Run the task + $result = $this->dep('packages:assets:vendors'); + $this->assertEquals(0, $result); + } + + public function testPackagesAssetsBuildOnlyRunsWhenAssetsIsTrue(): void + { + // Copy test plugin from fixtures to local + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin-with-assets'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin-without-assets'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin-without-assets-flag'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Create test packages configuration + $this->deployer->config->set('packages', [ + [ + 'path' => '{{mu-plugins/dir}}/test-plugin-with-assets', + 'assets' => true + ], + [ + 'path' => '{{mu-plugins/dir}}/test-plugin-without-assets', + 'assets' => false + ], + [ + 'path' => '{{mu-plugins/dir}}/test-plugin-without-assets-flag' + // No assets flag + ] + ]); + + // Mock npm build command + $this->mockCommands([ + 'npm run-script build' => function ($host, $command) { + if(str_contains($command, 'test-plugin-without-assets') || str_contains($command, 'test-plugin-without-assets-flag')) { + throw new RuntimeException('npm build called for plugin without assets/package.json'); + } + return 'npm build completed.'; + }, + ], 'testremote'); + + // Run the task + $result = $this->dep('packages:assets:build'); + $this->assertEquals(0, $result); + } + + public function testPackagesVendorsOnlyRunsWhenVendorsIsTrue(): void + { + // Copy test plugin from fixtures to local + $fixturePluginDir = $this->getFixturePath('mu-plugins/test-plugin'); + + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin-with-vendors'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin-without-vendors'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + $targetPluginDir = $this->localMuPluginsDir . '/test-plugin-without-vendors-flag'; + mkdir($targetPluginDir, 0755, true); + $this->copyDirectory($fixturePluginDir, $targetPluginDir); + + // Create test packages configuration + $this->deployer->config->set('packages', [ + [ + 'path' => '{{mu-plugins/dir}}/test-plugin-with-vendors', + 'vendors' => true + ], + [ + 'path' => '{{mu-plugins/dir}}/test-plugin-without-vendors', + 'vendors' => false + ], + [ + 'path' => '{{mu-plugins/dir}}/test-plugin-without-vendors-flag' + // No assets flag + ] + ]); + + // Mock npm install command + $this->mockCommands([ + 'composer install' => function ($host, $command) { + if(str_contains($command, 'test-plugin-without-vendors') || str_contains($command, 'test-plugin-without-vendors-flag')) { + throw new RuntimeException('composer install called for plugin without vendors'); + } + return 'npm install completed.'; + }, + ], 'testremote'); + + // Run the task + $result = $this->dep('packages:vendors'); + $this->assertEquals(0, $result); + } +} From 47cd9f0ec99820a2c59ed61fe3856d7e22000465 Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Fri, 20 Mar 2026 09:10:55 +0100 Subject: [PATCH 35/36] docs: finalize v4 docs --- CHANGELOG.md | 86 ++++++++----- README.md | 339 ++++++++++++++++----------------------------------- 2 files changed, 164 insertions(+), 261 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0637385..213dc11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,34 +1,56 @@ # Changelog -## v@next- YYYY-MM-DD +## v4.0.0 - 2025-03-20 + +v4 is a major refactor focusing on simpler, rsync-based deployments with better developer experience. + +_Info:_ This release does not yet officially support Deployer v8, since it's not released yet. + +### Breaking Changes + +- **Architecture**: Now uses `current_path` as primary deployment target instead of symlinked `release_path`. Atomic + releases via symlinks are still possible but no longer the default. +- **PSR-4 structure**: Internal code reorganized with PSR-4 autoloading. Task files moved from `src/tasks/` to `tasks/`. +- **Removed `advanced` recipe**: Use `simple` or `bedrock` recipes instead. +- **Removed `:sync` tasks**: Use explicit `:push` and `:pull` tasks instead. ### Added -- Introduced a new **packages** system for managing custom themes, plugins, and mu-plugins. This system allows for more flexible handling of assets, vendors, and deployment tasks. -- Improved path handling for various components. -- New tasks for deploying/syncing **language files**. -- Examples for Bedrock and simple deployment recipes to help users better understand and implement the new features. + +- **Packages system**: Unified way to manage custom themes, plugins, and mu-plugins with individual build scripts and + vendor configs. +- Older `themes`/`plugins`/`mu-plugins` are still available for backwards compatibility, but will be removed in a future + release. +- **Multisite support**: Set `wp/multisite` flag for proper URL replacements during database sync. +- **Language tasks**: New tasks for syncing language files (`languages:push`, `languages:pull`, `languages:backup:*`). +- **Localhost context**: Proper context handling for local operations with consistent path resolution. +- **New recipes**: `recipes/simple.php` for rsync deployments, `recipes/bedrock.php` for Bedrock projects. +- **Comprehensive tests**: Full integration and functional test suite. ### Changed -- Updated tasks to integrate with the new packages system, providing more flexibility and control over deployment processes. -- Refactored code to improve consistency and maintainability, particularly in handling local and remote paths. -### Deprecated -- Existing theme and plugin tasks remain functional, but the packages system offers enhanced capabilities and is the preferred method moving forward. +- Improved bin detection and auto-installation for `composer` and `wp-cli`. +- Enhanced database task logic for URL/path replacements. +- Better sudo prefix handling in binary execution. -### Removed -- advanced recipe (because it was basically the same as the simple) +### Fixed -**Upgrading:** -- Review and update your `set.php` configuration to include the new `packages` array for managing custom themes, plugins, and mu-plugins. -- Ensure that all paths in your configuration are correctly set relative to `release_path` or `current_path`. -- Consider migrating existing theme and plugin configurations to the new packages system for better flexibility and control. -- Test your deployment process in a staging environment to ensure compatibility with the new changes. -- Read the [README.md](README.md) section about packages for detailed guidance on configuration and usage. +- Database pulling and local importing. +- Recipe include paths when installed via composer. +- Path resolution in localhost operations. + +### Upgrading from v3.x + +1. **Set `current_path`**: Define where your WordPress root lives on each host (including localhost). +2. **Migrate to packages**: Move theme/plugin build config to the new `packages` configuration. +3. **Update recipe**: Switch from old recipes to `recipes/simple.php` or `recipes/bedrock.php`. +4. **Check examples**: Review `examples/simple/` or `examples/bedrock/` for updated patterns. +5. **Replace `:sync` tasks**: Update custom tasks using `:sync` to use `:push` or `:pull`. ## v3.1.0 - Added a `deploy:build_assets` step into the default deploy task to build theme assets on local. -This allows for easier overwriting this task (eg to build custom plugin assets) and fixes running duplicates on some configurations. + This allows for easier overwriting this task (eg to build custom plugin assets) and fixes running duplicates on some + configurations. ## v3.0.0 @@ -42,25 +64,33 @@ This allows for easier overwriting this task (eg to build custom plugin assets) - Removed `document_root` - use `release_or_current_path` instead - New/changed task names: - `push_code` now is called `update_code` again for parity with PHPDeployer. - + **Upgrading:** - - If you haven't upgraded to v2.0.0 yet, it's best to upgrade to 3.0.0 directly - - Have a look at the example files. Your deploy.php will get much smaller and require less configuration. - - Also the new version is more smiliar to PHPDeployers default common recipe. + +- If you haven't upgraded to v2.0.0 yet, it's best to upgrade to 3.0.0 directly +- Have a look at the example files. Your deploy.php will get much smaller and require less configuration. +- Also the new version is more smiliar to PHPDeployers default common recipe. ## v2.0.0 - Updated from Deployer 6.x to 7.x See [docs](https://deployer.org/docs/7.x/UPGRADE#upgrade-from-6x-to-7x) for more information. Most notable changes: - - New format for yml-files which can now also include configuration. - - The `local` is not available any more. Instead `once` and `runLocally` should be used. For theme assets the example uses a function callback and the `on` helper to optionally run those build tasks on the local host. - - When deploying you can't select a host by name or stage anymore. Instead you have to use labels (eg a `stage` label). If you've used `dep deploy production` you now have to use `dep deploy stage=production` and set the stage label in your yml file. + - New format for yml-files which can now also include configuration. + - The `local` is not available any more. Instead `once` and `runLocally` should be used. For theme assets the example + uses a function callback and the `on` helper to optionally run those build tasks on the local host. + - When deploying you can't select a host by name or stage anymore. Instead you have to use labels (eg a `stage` + label). If you've used `dep deploy production` you now have to use `dep deploy stage=production` and set the stage + label in your yml file. - Switched to a single base recipe which can be included and built upon. See `examples/deploy.php`. -- The new recipe and examples uses yml-files for project-specific configuration so the `deploy.php` is a dropin file and has no configuration in it. +- The new recipe and examples uses yml-files for project-specific configuration so the `deploy.php` is a dropin file and + has no configuration in it. - PHP 8 compatibility. - Fixes issues with rsync flags/options and `'`. **Upgrading:** -If you've used the default recipe it's probably easiest to copy the new example `deploy.php` and update your yml-file with project-specific configuration. If you have added any other tasks/features to your `deploy.php` make sure you upgrade them too. -If you've used most of the core functions of this library or just the examples, the upgrade should only take a few minutes. \ No newline at end of file +If you've used the default recipe it's probably easiest to copy the new example `deploy.php` and update your yml-file +with project-specific configuration. If you have added any other tasks/features to your `deploy.php` make sure you +upgrade them too. +If you've used most of the core functions of this library or just the examples, the upgrade should only take a few +minutes. diff --git a/README.md b/README.md index 79e00b0..b9254b8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # Deployer WordPress Recipes -A collection of [Deployer](https://deployer.org) Tasks/Recipes to deploy WordPress sites. From simple sites deployed via -copying files up to building custom themes/mu-plugins and installing npm/composer vendors - It handles all kinds of -WordPress installation types. +[Deployer](https://deployer.org) tasks and recipes for deploying WordPress sites. Supports simple rsync deployments, +custom theme/plugin builds, and complex setups including Bedrock and multisite. ## Table Of Contents @@ -12,102 +11,92 @@ WordPress installation types. - [Requirements](#requirements) - [Configuration](#configuration) - [Default Directory Structure](#default-directory-structure) + - [Localhost context](#localhost-context) - [wp-config.php](#wp-configphp) - [Rsync filters/excludes/includes](#rsync-filtersexcludesincludes) - [Tasks](#tasks) - [Database Tasks (`tasks/database.php`)](#database-tasks-tasksdatabasephp) + - [Multisite support](#multisite-support) + - [Packages system (`tasks/packages.php`)](#packages-system-taskspackagesphp) - [File Tasks (`tasks/files.php`)](#file-tasks-tasksfilesphp) - - [Theme Tasks (`tasks/theme.php`)](#theme-tasks-tasksthemephp) - - [Uploads Tasks (`tasks/uploads.php`)](#uploads-tasks-tasksuploadsphp) - - [Plugin Tasks (`tasks/plugins.php`)](#plugin-tasks-taskspluginsphp) - - [MU Plugin Tasks (`tasks/mu-plugins.php`)](#mu-plugin-tasks-tasksmu-pluginsphp) - [WordPress Tasks (`tasks/wp.php`)](#wordpress-tasks-taskswpphp) - [Language Tasks (`tasks/languages.php`)](#language-tasks-taskslanguagesphp) - - [WP-CLI](#wp-cli) - - [Recipes](#recipes) - - [Base](#base) - - [Advanced](#advanced) - - [Custom Theme](#custom-theme) - - [Custom MU-Plugin](#custom-mu-plugin) + - [Uploads Tasks (`tasks/uploads.php`)](#uploads-tasks-tasksuploadsphp) + - [Legacy Tasks (Themes, Plugins, MU-Plugins)](#legacy-tasks-themes-plugins-mu-plugins) + - [WP-CLI](#wp-cli) + - [Recipes](#recipes) + - [Simple](#simple) + - [Bedrock](#bedrock) - [Changelog](#changelog) - [Contributing](#contributing) - [Testing](#testing) - [Built by](#built-by) - - [Packages](#packages) - - [Configuration](#configuration) - - [Example](#example) - - [Best Practices](#best-practices) ## Installation -1. Run `composer require gaambo/deployer-wordpress --dev` in your root directory -2. Choose one of the [recipes](#recipes) and copy the corresponding example files (`examples/base` or - `examples/advanced`) into your root directory - **or** write your own. -3. Read through the recipe and customize it to your needs - here's a checklist: +1. `composer require gaambo/deployer-wordpress --dev` +2. Copy example files from `examples/simple/` or `examples/bedrock/` to your project root +3. Customize `deploy.php` and `deploy.yml`: -- [ ] Check localhost configuration -- [ ] Set paths to your directory structure -- [ ] If you have a custom theme set it's name - if not remove the configuration and the theme build-tasks -- [ ] If you have a custom mu-plugin set it's name - if not remove the configuration and the mu-plugin build-tasks -- [ ] Check if the deployment flow meets your needs and maybe delete/add/overwrite tasks +- Set `current_path` for all hosts (remote + localhost) +- Configure custom code as [packages](#packages-system-taskspackagesphp) +- Adjust deployment flow if needed -4. Make your remote hosts ready for deployment (install composer, WP CLI; setup paths,...). Allthough the library checks - for most of them and installs them. -5. Make a **test deployment** to a test/staging server. Do not directly deploy to your production site, you may break - it. -6. Develop, deploy and be happy :) +4. Test on staging first, then deploy to production 🚀 ## Requirements -Obviously: +- PHP + [Composer](https://getcomposer.org) +- [Deployer](https://deployer.org) (automatically installed) +- WordPress installation +- *nix OS (Linux/macOS or [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) on Windows) +- `rsync` installed -- [PHP](https://php.net/) and [composer](https://getcomposer.org) for installing and using Deployer -- [Deployer](https://deployer.org) core (`deployer/deployer`) is required dependencies of this package defined in - `composer.json` -- WordPress installation + local web server and database to use it +Optional (auto-installed on remote if missing): -Most of the tasks only run in *nix shells - so a *nix **operating system** is preferred. If you run Windows have a look -at [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) to run Ubuntu Bash inside Windows. +- [WP-CLI](https://wp-cli.org/) for database tasks +- [Composer](https://getcomposer.org) for package vendors (depending on your custom theme/plugins) +- [Node.js/npm](https://nodejs.org/) for building assets (depending on your custom theme/plugins) -Some tasks have additional requirements, eg: +## Configuration -- [composer](https://getcomposer.org) for PHP dependencies -- [Node.js/npm](https://nodejs.org/en/) for JavaScript dependencies -- [WP CLI](https://wp-cli.org/de/) for database backups/exports/imports -- `rsync` and `zip` command installed +Example recipes (in `examples/`) provide configuration starting points. The library works with any WordPress setup ( +vanilla, Composer, subdirectory, Bedrock, multisite) by making all paths and directories configurable. Check the example +recipes and task source files for available options. -## Configuration +### Localhost context -All tasks are documented and describe which options/variables need to be configured. `set.php` is included in all -example recipes - This and the example recipes should have you covered regarding all required configurations. Further -variables which need to be set by you are marked accordingly in the recipes. +Version 4 introduces a proper `Localhost` context. This ensures that when tasks run on your local machine +(like building assets or backing up the local database), they use configuration values specifically defined for +localhost, rather than falling back to global or remote values. -To help understand all the configurations here are the thoughts behind theme: -The tasks are built to work with any kind of WordPress setup (vanilla, composer, subdirectory,..) - therefore all paths -and directorys are configurable via variables. `set.php` contains some sane defaults which makes all tasks work out of -the box with a default installation. +Configure localhost in `deploy.php`: -### Default Directory Structure +```php +localhost() + ->set('public_url', 'http://wp-boilerplate.test') + ->set('current_path', 'public') // WordPress root + ->set('dbdump_path', __DIR__ . '/data/db_dumps') + ->set('backup_path', __DIR__ . '/data/backups'); +``` -My [Vanilla WordPress Boilerplate](https://github.com/gaambo/vanilla-wp/) uses this library. You can find a example -configuration in the GitHub repository. +The `Localhost` utility class (`Gaambo\DeployerWordpress\Localhost`) handles switching context automatically in tasks. -### wp-config.php +### wp-config.php (Recommendation) -To make WordPress deployable you need to extract the host-dependent configuration (eg database access) into a seperate -file which does not live in your git repository and is not deployed. I suggest using a **`wp-config-local.php`** file. -This file should be required in your `wp-config.php` and be ignored by git (via `.gitignore`). This way `wp-config.php` -can (should) be in your git repository and also be deployed. The default `wp/filter` configuration assumes this. -Another advantage of using a `wp-config-local.php` is to set `WP_DEBUG` on a per host basis. +Keep `wp-config.php` in git and deploy it. Extract environment-specific config (database credentials, `WP_DEBUG`) into +`wp-config-local.php`, which should be gitignored and created manually on each host. Require it from `wp-config.php`: -### Rsync filters/excludes/includes +```php +if (file_exists(__DIR__ . '/wp-config-local.php')) { + require_once __DIR__ . '/wp-config-local.php'; +} +``` -The default rsync config for syncing files (used by all \*:push/\*:pull tasks) is set in the `rsync` variable. -By default it set's a `filter-perDir` argument as `.deployfilter` - which means rsync will look for a file named -`.deployfilter` in each directory to parse filters for this directory. -See [rsync man](https://linux.die.net/man/1/rsync) section "Filter Rules" for syntax. +### Rsync filters -This can be handy to put int your custom theme or mu-plugin - for example: +The default rsync config uses `.deployfilter` files for per-directory filtering. Place a `.deployfilter` file in your +theme/plugin to exclude development files: ``` - phpcs.xml @@ -122,6 +111,8 @@ This can be handy to put int your custom theme or mu-plugin - for example: - gulpfile.babel.js - package.json - package-lock.json +- .babelrc +- phpcs.xml ``` This prevents any development files/development tools from syncing. I strongly recommend you put something like this in @@ -129,9 +120,8 @@ your custom theme and mu-plugins or overwrite any of the `themes/filter` or `mu- ## Tasks -All tasks reside in the `src/tasks` directory and are documented well. Here's a summary of all tasks - for details (eg -required variables/config) see their source. -You can also run `dep list` to see all available tasks and their description. +Tasks are in the `tasks/` directory. Run `dep list` to see all available tasks. See task source files for configuration +options. ### Database Tasks (`tasks/database.php`) @@ -142,135 +132,70 @@ You can also run `dep list` to see all available tasks and their description. - `db:push`: Pushes local database to remote host (combines `db:local:backup` and `db:remote:import`) - `db:pull`: Pulls remote database to localhost (combines `db:remote:backup` and `db:local:import`) -### File Tasks (`tasks/files.php`) - -- `files:push`: Pushes all files from local to remote host (combines `wp:push`, `uploads:push`, `plugins:push`, - `mu-plugins:push`, `themes:push`) -- `files:pull`: Pulls all files from remote to local host (combines `wp:pull`, `uploads:pull`, `plugins:pull`, - `mu-plugins:pull`, `themes:pull`) - -### Theme Tasks (`tasks/theme.php`) +#### Multisite support -> **Note:** It is recommended to use the new packages functionality for managing themes for better flexibility and -> control. +For multisite installations, set `wp/multisite` to `true` to enable network-wide search-replace during database sync: -- `theme:assets:vendors`: Install theme assets vendors/dependencies (npm), can be run locally or remote -- `theme:assets:build`: Run theme assets (npm) build script, can be run locally or remote -- `theme:assets`: A combined tasks to build theme assets - combines `theme:assets:vendors` and `theme:assets:build` -- `theme:vendors`: Install theme vendors (composer), can be run locally or remote -- `theme`: A combined task to prepare the theme - combines `theme:assets` and `theme:vendors` -- `themes:push`: Push themes from local to remote -- `themes:pull`: Pull themes from remote to local -- `themes:sync`: Syncs themes between remote and local -- `themes:backup:remote`: Backup themes on remote host and download zip -- `themes:backup:local`: Backup themes on localhost - -### Uploads Tasks (`tasks/uploads.php`) +```php +set('wp/multisite', true); +``` -- `uploads:push`: Push uploads from local to remote -- `uploads:pull`: Pull uploads from remote to local -- `uploads:sync`: Syncs uploads between remote and local -- `uploads:backup:remote`: Backup uploads on remote host and download zip -- `uploads:backup:local`: Backup uploads on localhost +### Packages system (`tasks/packages.php`) -### Plugin Tasks (`tasks/plugins.php`) +Manage custom themes, plugins, and mu-plugins with individual build configs: -> **Note:** It is recommended to use the new packages functionality for managing plugins for better flexibility and -> control. +```php +set('packages', [ + 'custom-theme' => [ + 'path' => '{{themes/dir}}/custom-theme', + 'remote:path' => '{{themes/dir}}/custom-theme', // optional + 'assets' => true, + 'assets:build_script' => 'build', + 'vendors' => true, // Run composer install + ], + // Add more packages as needed +]); +``` -- `plugins:push`: Push plugins from local to remote -- `plugins:pull`: Pull plugins from remote to local -- `plugins:sync`: Syncs plugins between remote and local -- `plugins:backup:remote`: Backup plugins on remote host and download zip -- `plugins:backup:local`: Backup plugins on localhost +**Tasks:** -### MU Plugin Tasks (`tasks/mu-plugins.php`) +- `packages:assets:vendors` - Install npm dependencies +- `packages:assets:build` - Run build scripts +- `packages:vendors` - Install composer dependencies +- `packages:push` / `packages:pull` - Sync packages -> **Note:** It is recommended to use the new packages functionality for managing mu-plugins for better flexibility and -> control. +### Other Task Categories -- `mu-plugin:vendors`: Install mu-plugin vendors (composer), can be run locally or remote -- `mu-plugin`: A combined tasks to prepare the theme - at the moment only runs mu-plugin:vendors task -- `mu-plugins:push`: Push mu-plugins from local to remote -- `mu-plugins:pull`: Pull mu-plugins from remote to local -- `mu-plugins:sync`: Syncs mu-plugins between remote and local -- `mu-plugins:backup:remote`: Backup mu-plugins on remote host and download zip -- `mu-plugins:backup:local`: Backup mu-plugins on localhost +**File Tasks** (`tasks/files.php`) -### WordPress Tasks (`tasks/wp.php`) +- `files:push` / `files:pull` - Sync all files (combines wp, uploads, plugins, themes, packages) -- `wp:download-core`: Installs WordPress core via WP CLI -- `wp:push`: Pushes WordPress core files via rsync -- `wp:pull`: Pulls WordPress core files via rsync -- `wp:info`: Runs the --info command via WP CLI - just a helper/test task -- `wp:install-wpcli`: Install the WP-CLI binary manually with the `wp:install-wpcli` task and set the path as `/bin/wp` - afterwards. +**WordPress Core** (`tasks/wp.php`) -#### WP-CLI +- `wp:download-core`, `wp:push`, `wp:pull`, `wp:info` -Handling and installing the WP-CLI binary can be done in one of multiple ways: +**Languages** (`tasks/languages.php`) -1. The default `bin/wp` in `set.php` checks for a usable WP-CLI binary and if none is found it downloads and installs it - to `{{deploy_path}}/.dep/wp-cli.phar` (this path is checked in the future as well). -2. If you want this behaviour (check if installed, else install) but in another path, overwrite the `bin/wp` variable - with a function and change the path it should be installed to. -3. Set the `bin/wp` variable path on the host configuration, if WP-CLI is already installed. -4. Install the WP-CLI binary manually with the `wp:install-wpcli` task and set the path as `/bin/wp` afterwards. - You can pass the installPath, binaryFile and sudo usage via CLI: - `dep wp:install-wpcli stage=production -o installPath='{{deploy_path}}/.bin -o binaryFile=wp -o sudo=true` +- `languages:push`, `languages:pull`, `languages:sync`, `languages:backup:*` -See [original PR](https://github.com/gaambo/deployer-wordpress/pull/5) for more information. +**Uploads** (`tasks/uploads.php`) -There's a task for downloading core and `--info`. You can generate your own tasks to handle other WP-CLI commands, -there's a util function `Gaambo\DeployerWordpress\Utils\WPCLI\runCommand` (`src/utils/wp-cli.php`); +- `uploads:push`, `uploads:pull`, `uploads:sync`, `uploads:backup:*` -### Language Tasks (`tasks/languages.php`) +**Legacy Tasks** (use [packages](#packages-system-taskspackagesphp) instead) -- `languages:push`: Push language files from local to remote -- `languages:pull`: Pull language files from remote to local -- `languages:sync`: Sync language files between remote and local -- `languages:backup:remote`: Backup language files on remote host and download zip -- `languages:backup:local`: Backup language files on localhost +- Themes: `themes:push`, `themes:pull` +- Plugins: `plugins:push`, `plugins:pull` +- MU-Plugins: `mu-plugins:push`, `mu-plugins:pull` ## Recipes -Deployer WordPress ships with two base recipes which handle my use cases. -Both recipes are based on the default PHPDeployer common recipe and have their own recipe file which you can include in -your `deploy.php` as a start. The examples folder provides examples for each recipe. -Both recipes log the deployed versions in PHPDeployers default format (`.dep` folder). -Both recipes overwrites the `deploy:update_code` Deployer task with a `deploy:update_code` task to deploy code via rsync -instead of git - -### Base - -This is for WordPress sites where you don't need symlinking per version or atomic releases. This means that on your -remote/production host you just have on folder which contains all of WordPress files and this is served by your web -server. -Since this is still based on the default PHPDeployer recipe which uses symlinking and to provide compatibility with all -tasks, this just hardcodes the `release_path` and `current_path`. - -### Advanced - -This uses symlinking like the default common recipe from PHPDeployer. Each release gets deployed in its own folder unter -`{{deploy_path}}/releases/` and `{{deploy_path}}/current` is a symlink to the most current version. The symlink is -automatically updated after the deployment finishes successfully. You can configure your webserver to just server -`{{deploy_path}}/current`. - -#### Custom Theme +v4 uses `current_path` for rsync-based deployments. Symlinked releases are still possible but not the default. -Set custom theme name (= directory) in variable `theme/name`. -By default it runs `theme:assets:vendors` and `theme:assets:build` locally and just pushes the built/dist files to the -server (--> no need to install Node.js/npm on server). The `theme:assets` task (which groups the two tasks above) is -hooked into _before_ `deploy:push_code`. +**`recipes/simple.php`** - Standard WordPress. Rsyncs directly to `current_path`. Recommended for most projects. -Installing PHP/composer vendors/dependencies is done on the server. The `theme:vendors` task is therefore hooked into -_after_ `deploy:push_code`. - -#### Custom MU-Plugin - -Set custom mu-plugin name (=directory) in variable `mu-plugin/name`. -Installing PHP/composer vendors/dependencies is done on the server. The `mu-plugin:vendors` task is therefore hooked -into _after_ `deploy:push_code`. +**`recipes/bedrock.php`** - [Roots Bedrock](https://roots.io/bedrock/) projects with appropriate structure and +environment handling. ## Changelog @@ -278,65 +203,13 @@ See [CHANGELOG.md](CHANGELOG.md). ## Contributing -If you have feature requests, find bugs or need help just open an issue -on [GitHub](https://github.com/gaambo/deployer-wordpress). -Pull requests are always welcome. PSR2 coding standard are used and I try to adhere to Deployer best-practices. +Issues, feature requests, and pull requests welcome at [GitHub](https://github.com/gaambo/deployer-wordpress). Code +follows PSR-2 and Deployer best practices. ### Testing -1. Download my [Vanilla WordPress Boilerplate](https://github.com/gaambo/vanilla-wp/) or set up a local dev environment - with a deploy config -2. Setup a remote test server -3. Configure yml/deploy.php -4. Run common tasks (`deploy`, `plugins:pull/push`, `db`) for both base as well as advanced recipe - -## Built by - -[Gaambo](https://github.com/gaambo) and [Contributors](https://github.com/gaambo/deployer-wordpress/graphs/contributors) - -## Packages (added in v@next) - -The new packages system allows for more flexible handling of custom themes, plugins, and mu-plugins. Packages can be -configured to manage assets, vendors, and deployment tasks. - -### Configuration - -To configure packages, add the following to your `set.php` or equivalent configuration file: - -```php -set('packages', [ - [ - 'path' => 'path/to/package', - 'remote:path' => 'path/on/remote', - 'assets' => true, - 'assets:build_script' => 'build', - 'vendors' => true, - ], - // Add more packages as needed -]); -``` - -### Example - -Here is an example configuration for a custom theme package: - -```php -set('packages', [ - 'custom-theme' => [ - 'path' => '{{themes/dir}}/custom-theme', - 'remote:path' => '{{themes/dir}}/custom-theme', - 'assets' => true, - 'assets:build_script' => 'build' - ], - 'core-functionality' => [ - 'path' => '{{mu-plugins/dir}}/core-functionality', - 'remote:path' => '{{mu-plugins/dir}}/core-functionality', - 'vendors' => true, - ], -]); -``` - -### Best Practices +The library includes a comprehensive test suite with unit, integration, and functional tests. -- Ensure all package paths are correctly set relative to the `release_path` or `current_path`. -- Define npm build scripts in your `package.json` for asset management. +- Run `composer phpunit` to execute all tests. +- Functional tests use a mocked environment to verify rsync commands and file operations without real remote + connections. From cad0c3096a0fa1665364ed967e8b4cecc1fc347e Mon Sep 17 00:00:00 2001 From: Fabian Todt Date: Fri, 20 Mar 2026 09:29:02 +0100 Subject: [PATCH 36/36] fix github action test composer script --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 872d038..ebf58ac 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,9 +11,9 @@ jobs: timeout-minutes: 10 strategy: matrix: - php: ['8.1', '8.2', '8.3', '8.4'] + php: [ '8.1', '8.2', '8.3', '8.4' ] # TODO: Add 8.0 when released. - deployer: ['7.3', '7.4', '7.5'] + deployer: [ '7.3', '7.4', '7.5' ] fail-fast: false name: Tests for Deployer ${{ matrix.deployer }} on PHP ${{ matrix.php }} @@ -46,4 +46,4 @@ jobs: # Run PHPUnit tests - name: Run PHPUnit tests - run: composer run-script test + run: composer run-script tests