diff --git a/docs/recipe/magento2.md b/docs/recipe/magento2.md index 98176c9d3..6ea573866 100644 --- a/docs/recipe/magento2.md +++ b/docs/recipe/magento2.md @@ -371,8 +371,30 @@ return false; ``` +### theme_fingerprint_file +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L181) + +Where the themes registered by the last deploy are recorded, so the next deploy can tell +whether the set of themes changed. + +```php title="Default value" +'{{deploy_path}}/.dep/theme_fingerprint' +``` + + +### theme_registration_needed +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L183) + + +:::info Autogenerated +The value of this configuration is autogenerated on access. +::: + + + + ### upgrade_needed -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L179) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L200) :::info Autogenerated @@ -383,7 +405,7 @@ The value of this configuration is autogenerated on access. ### enable_zerodowntime -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L189) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L210) Deploy without setting maintenance mode if possible @@ -393,7 +415,7 @@ true ### artifact_file -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L422) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L483) Artifact deployment section The file the artifact is saved to @@ -404,7 +426,7 @@ The file the artifact is saved to ### artifact_dir -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L425) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L486) The directory the artifact is saved in @@ -414,7 +436,7 @@ The directory the artifact is saved in ### artifact_excludes_file -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L429) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L490) Points to a file with a list of files to exclude from packaging. The format is as with the `tar --exclude-from=[file]` option @@ -425,7 +447,7 @@ The format is as with the `tar --exclude-from=[file]` option ### build_from_repo -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L432) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L493) If set to true, the artifact is built from a clean copy of the project repository instead of the current working directory @@ -435,7 +457,7 @@ false ### repository -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L435) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L496) Overrides [repository](/docs/recipe/common.md#repository) from `recipe/common.php`. @@ -447,7 +469,7 @@ null ### artifact_path -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L438) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L499) The relative path to the artifact file. If the directory does not exist, it will be created @@ -460,7 +482,7 @@ return get('artifact_dir') . '/' . get('artifact_file'); ### bin/tar -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L446) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L507) The location of the tar command. On MacOS you should have installed gtar, as it supports the required settings :::info Autogenerated @@ -471,14 +493,14 @@ The value of this configuration is autogenerated on access. ### additional_shared_files -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L518) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L579) Array of shared files that will be added to the default shared_files without overriding ### additional_shared_dirs -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L520) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L581) Array of shared directories that will be added to the default shared_dirs without overriding @@ -488,7 +510,7 @@ Array of shared directories that will be added to the default shared_dirs withou ## Tasks ### magento\:compile {#magento-compile} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L199) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L220) Compiles magento di. @@ -501,7 +523,7 @@ e.g. ### magento\:deploy\:assets {#magento-deploy-assets} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L225) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L246) Deploys assets. @@ -528,7 +550,7 @@ in `app/etc/config.php`, e.g.: ### magento\:deploy\:assets\:adminhtml {#magento-deploy-assets-adminhtml} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L242) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L263) Deploys assets for backend only. @@ -536,7 +558,7 @@ Deploys assets for backend only. ### magento\:deploy\:assets\:frontend {#magento-deploy-assets-frontend} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L247) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L268) Deploys assets for frontend only. @@ -544,7 +566,7 @@ Deploys assets for frontend only. ### magento\:sync\:content_version {#magento-sync-content_version} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L308) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L348) Syncs content version. @@ -552,7 +574,7 @@ Syncs content version. ### magento\:maintenance\:enable {#magento-maintenance-enable} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L318) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L358) Enables maintenance mode. @@ -560,7 +582,7 @@ Enables maintenance mode. ### magento\:maintenance\:disable {#magento-maintenance-disable} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L324) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L364) Disables maintenance mode. @@ -568,7 +590,7 @@ Disables maintenance mode. ### magento\:maintenance\:enable-if-needed {#magento-maintenance-enable-if-needed} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L330) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L370) Set maintenance mode if needed. @@ -576,7 +598,7 @@ Set maintenance mode if needed. ### magento\:config\:import {#magento-config-import} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L337) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L377) Config Import. @@ -584,7 +606,7 @@ Config Import. ### magento\:config\:import\:on-current {#magento-config-import-on-current} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L346) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L386) Config Import on current release. @@ -592,15 +614,31 @@ Config Import on current release. ### magento\:upgrade\:db {#magento-upgrade-db} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L360) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L400) Upgrades magento database. +### magento\:theme\:register {#magento-theme-register} +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L412) + +Registers the themes of this release. + + + + +### magento\:theme\:fingerprint {#magento-theme-fingerprint} +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L422) + +Records the themes registered for this release. + + + + ### magento\:upgrade {#magento-upgrade} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L372) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L429) Run upgrades if needed. @@ -608,7 +646,7 @@ Run upgrades if needed. ### magento\:cache\:flush {#magento-cache-flush} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L381) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L442) Flushes Magento Cache. @@ -616,7 +654,7 @@ Flushes Magento Cache. ### deploy\:magento {#deploy-magento} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L386) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L447) Magento2 deployment operations. @@ -632,7 +670,7 @@ This task is group task which contains next tasks: ### magento\:build {#magento-build} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L395) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L456) Magento2 build operations. @@ -645,7 +683,7 @@ This task is group task which contains next tasks: ### deploy {#deploy} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L401) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L462) Deploys your project. @@ -661,7 +699,7 @@ This task is group task which contains next tasks: ### deploy\:magento\:failed {#deploy-magento-failed} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L414) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L475) Run Magento post-deployment failure tasks. @@ -674,7 +712,7 @@ This task is group task which contains next tasks: ### artifact\:package {#artifact-package} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L457) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L518) Packages all relevant files in an artifact. @@ -682,7 +720,7 @@ tasks section ### artifact\:upload {#artifact-upload} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L467) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L528) Uploads artifact in release folder for extraction. @@ -690,7 +728,7 @@ Uploads artifact in release folder for extraction. ### artifact\:extract {#artifact-extract} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L472) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L533) Extracts artifact in release path. @@ -698,7 +736,7 @@ Extracts artifact in release path. ### build\:remove-generated {#build-remove-generated} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L478) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L539) Clears generated files prior to building. @@ -706,7 +744,7 @@ Clears generated files prior to building. ### build\:prepare {#build-prepare} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L483) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L544) Prepare local artifact build. @@ -714,7 +752,7 @@ Prepare local artifact build. ### artifact\:build {#artifact-build} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L508) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L569) Builds an artifact. @@ -731,7 +769,7 @@ This task is group task which contains next tasks: ### deploy\:additional-shared {#deploy-additional-shared} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L524) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L585) Adds additional files and dirs to the list of shared files and dirs. @@ -739,7 +777,7 @@ Adds additional files and dirs to the list of shared files and dirs. ### magento\:set_cache_prefix {#magento-set_cache_prefix} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L539) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L600) Update cache id_prefix. @@ -753,7 +791,7 @@ after('deploy:magento', 'magento:cleanup_cache_prefix'); ### magento\:cleanup_cache_prefix {#magento-cleanup_cache_prefix} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L579) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L640) Cleanup cache id_prefix env files. @@ -761,7 +799,7 @@ After successful deployment, move the tmp_env.php file to env.php ready for next ### magento\:cron\:stop {#magento-cron-stop} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L595) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L656) Remove cron from crontab and kill running cron jobs. @@ -773,7 +811,7 @@ To use this feature, add the following to your deployer scripts: ### magento\:cron\:install {#magento-cron-install} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L611) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L672) Install cron in crontab. @@ -785,7 +823,7 @@ To use this feature, add the following to your deployer scripts: ### artifact\:prepare {#artifact-prepare} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L617) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L678) Prepares an artifact on the target server. @@ -805,7 +843,7 @@ This task is group task which contains next tasks: ### artifact\:finish {#artifact-finish} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L630) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L691) Executes the tasks after artifact is released. @@ -821,7 +859,7 @@ This task is group task which contains next tasks: ### artifact\:deploy {#artifact-deploy} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L639) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/magento2.php#L700) Actually releases the artifact deployment. diff --git a/recipe/magento2.php b/recipe/magento2.php index 12d612657..6da2a3d59 100644 --- a/recipe/magento2.php +++ b/recipe/magento2.php @@ -176,10 +176,31 @@ return false; }); +// Where the themes registered by the last deploy are recorded, so the next deploy can tell +// whether the set of themes changed. +set('theme_fingerprint_file', '{{deploy_path}}/.dep/theme_fingerprint'); + +set('theme_registration_needed', function () { + // A db or full upgrade registers the themes on its way through, so there is nothing to add. + if (get('database_upgrade_needed') || get('full_upgrade_needed')) { + return false; + } + + $fingerprint = magentoThemeFingerprint('{{release_or_current_path}}'); + + if (test('[ -f {{theme_fingerprint_file}} ]')) { + return $fingerprint !== run('cat {{theme_fingerprint_file}}'); + } + + // Nothing recorded yet, so fall back to the release that is live. That keeps the first + // deploy after upgrading Deployer from registering themes it has no reason to touch. + return has('previous_release') && $fingerprint !== magentoThemeFingerprint('{{previous_release}}'); +}); + set('upgrade_needed', function () { // Detect necessary upgrade, partial db or full upgrade try { - return get('database_upgrade_needed') || get('full_upgrade_needed'); + return get('database_upgrade_needed') || get('full_upgrade_needed') || get('theme_registration_needed'); } catch (RunException $e) { throw $e; } @@ -248,6 +269,25 @@ magentoDeployAssetsSplit('frontend'); }); +/** + * Fingerprints the themes of a release: which theme.xml files it has and what they declare. + * A different fingerprint means themes were added, removed or re-parented, so the theme table + * no longer describes what is on disk. + */ +function magentoThemeFingerprint(string $path): string +{ + $themes = run("cd $path/{{magento_dir}} && find vendor app/design -maxdepth 6 -name theme.xml 2>/dev/null | sort"); + + if ($themes === '') { + return ''; + } + + $paths = array_map('escapeshellarg', explode("\n", $themes)); + $declarations = run("cd $path/{{magento_dir}} && cat " . implode(' ', $paths)); + + return md5($themes . $declarations); +} + /** * @throws ConfigurationException */ @@ -368,13 +408,34 @@ function magentoDeployAssetsSplit(string $area): void } }); +desc('Registers the themes of this release'); +task('magento:theme:register', function () { + // Themes are registered by Magento_Theme's recurring data script, which stores the themes + // found on disk and re-points the children of a theme that is gone. It runs as part of the + // data upgrade, which setup:db:status does not ask for when only the themes changed. The + // theme table then keeps pointing at a directory that no longer exists, and every page + // fails with "Required parameter 'theme_dir' was not passed". + run("{{bin/php}} {{bin/magento}} setup:db-data:upgrade --no-interaction"); +}); + +desc('Records the themes registered for this release'); +task('magento:theme:fingerprint', function () { + $fingerprint = magentoThemeFingerprint('{{release_or_current_path}}'); + + run('mkdir -p $(dirname {{theme_fingerprint_file}}) && echo ' . escapeshellarg($fingerprint) . ' > {{theme_fingerprint_file}}'); +}); + desc('Run upgrades if needed'); task('magento:upgrade', function () { if (get('full_upgrade_needed')) { run("{{bin/php}} {{bin/magento}} setup:upgrade --no-interaction --keep-generated"); } elseif (get('database_upgrade_needed')) { invoke('magento:upgrade:db'); + } elseif (get('theme_registration_needed')) { + invoke('magento:theme:register'); } + + invoke('magento:theme:fingerprint'); })->once(); desc('Flushes Magento Cache'); diff --git a/tests/spec/MagentoThemeTest.php b/tests/spec/MagentoThemeTest.php new file mode 100644 index 000000000..1feff07fa --- /dev/null +++ b/tests/spec/MagentoThemeTest.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer; + +use spec\SpecTest; +use Symfony\Component\Console\Output\Output; + +class MagentoThemeTest extends SpecTest +{ + public const RECIPE = __DIR__ . '/recipe/magento_theme.php'; + + public function testDetectsWhichReleasesNeedThemeRegistration() + { + $this->createRelease('removed/release', ['Magento/blank', 'Acme/default']); + $this->createRelease('removed/previous', ['Magento/blank', 'Acme/default', 'Acme/reset']); + + $this->createRelease('reparented/release', ['Acme/default'], 'Magento/blank'); + $this->createRelease('reparented/previous', ['Acme/default'], 'Acme/reset'); + + $this->createRelease('unchanged/release', ['Magento/blank', 'Acme/default']); + $this->createRelease('unchanged/previous', ['Magento/blank', 'Acme/default']); + + $this->createRelease('recorded/release', ['Magento/blank', 'Acme/default']); + $this->createRelease('recorded/previous', ['Magento/blank']); + + $this->init(self::RECIPE); + + $this->runTask('magento:theme:fingerprint', 'recorded'); + self::assertFileExists(__TEMP_DIR__ . '/magento-theme/recorded/theme_fingerprint'); + + $display = $this->runTask('test_theme_registration_needed', 'all'); + + self::assertMatchesRegularExpression('/\[removed\] REGISTRATION NEEDED/', $display); + self::assertMatchesRegularExpression('/\[reparented\] REGISTRATION NEEDED/', $display); + self::assertMatchesRegularExpression('/\[unchanged\] REGISTRATION NOT NEEDED/', $display); + + // The themes this release registered are recorded, so the previous release, which has + // one theme less, is no longer the thing to compare against. + self::assertMatchesRegularExpression('/\[recorded\] REGISTRATION NOT NEEDED/', $display); + } + + /** + * @param list $themes + */ + private function createRelease(string $path, array $themes, ?string $parent = null): void + { + $release = __TEMP_DIR__ . '/magento-theme/' . $path; + $parent = $parent === null ? '' : "$parent"; + + mkdir($release . '/app/design/frontend', 0777, true); + + foreach ($themes as $theme) { + mkdir($release . '/vendor/themes/' . $theme, 0777, true); + file_put_contents( + $release . '/vendor/themes/' . $theme . '/theme.xml', + "$theme$parent", + ); + } + } + + private function runTask(string $task, string $selector): string + { + $this->tester->run([ + $task, + 'selector' => $selector, + '-f' => self::RECIPE, + ], [ + 'verbosity' => Output::VERBOSITY_VERBOSE, + ]); + + $display = $this->tester->getDisplay(); + self::assertEquals(0, $this->tester->getStatusCode(), $display); + + return $display; + } +} diff --git a/tests/spec/recipe/magento_theme.php b/tests/spec/recipe/magento_theme.php new file mode 100644 index 000000000..fae35f1e4 --- /dev/null +++ b/tests/spec/recipe/magento_theme.php @@ -0,0 +1,23 @@ +set('release_or_current_path', "$cases/$case/release") + ->set('previous_release', "$cases/$case/previous") + ->set('theme_fingerprint_file', "$cases/$case/theme_fingerprint") + ->set('database_upgrade_needed', false) + ->set('full_upgrade_needed', false); +} + +task('test_theme_registration_needed', function () { + writeln(get('theme_registration_needed') ? 'REGISTRATION NEEDED' : 'REGISTRATION NOT NEEDED'); +});