diff --git a/deploy.php b/deploy.php index 8fe3bcb..7671c19 100644 --- a/deploy.php +++ b/deploy.php @@ -2,29 +2,13 @@ namespace Deployer; -// Run on the host (Deployer 8, PHP >= 8.3): dep deploy prod. The SSH keys stay on the host; the -// build runs inside DDEV, so Composer and Node are the project's versions. -// Prerequisites on the host: see .ddev/plans/TYPO3-13-GO-LIVE.md (PHP 8.5.12, database, secrets.php) - require 'recipe/typo3.php'; require 'contrib/rsync.php'; set('application', 'ep-reisen-typo3-13'); - -// Defined here rather than taken from recipe/typo3.php, whose task and setting names differ between -// Deployer versions (a globally installed "dep" may not be the project's vendor/bin/dep 8) -set('typo3/public_dir', 'public'); -set('bin/typo3', 'vendor/bin/typo3'); set('keep_releases', 3); -// The release is built locally from the committed state, so nothing uncommitted is ever deployed -// and the host needs neither Composer nor Node -set('build_dir', __DIR__ . '/var/deploy-build'); -set('use_rsync', true); -set('rsync_src', '{{build_dir}}'); - -// fileadmin is the production fileadmin of the TYPO3 10 release: it is the CDN origin's fileadmin -// (static/ep-reisen/fileadmin links there too), and TYPO3 13 writes its processed images into it +// fileadmin of the TYPO3 10 release until it is moved into shared/ (plan 4a, tidy up step 2) set('fileadmin_path', '/usr/home/eptypo3/public_html/ep-reisen-typo3/shared/public/fileadmin'); set('shared_dirs', [ @@ -36,10 +20,7 @@ set('shared_dirs', [ // Baseline of the MyE&P import: without it every deploy would trigger a full re-import 'var/transient/ep_products_import', ]); -// settings.php is shared as LocalConfiguration.php was on TYPO3 10: production is leading, so what -// is changed in the backend (extension configuration, install tool) survives deploys and rollbacks. -// It is not in git (encryptionKey, install tool password): configuration changes for production are -// made there, in the backend or by hand on the shared file. +// Not in git, production is leading (encryptionKey, install tool, backend changes) set('shared_files', [ 'config/system/secrets.php', 'config/system/settings.php', @@ -53,6 +34,13 @@ set('writable_dirs', [ ]); set('writable_mode', 'chmod'); +// The release is built from the committed state, so nothing uncommitted and no dev dependencies go live +set('build_dir', __DIR__ . '/var/deploy-build'); +set('use_rsync', true); +set('rsync_src', '{{build_dir}}'); + +// contrib/rsync.php replaces the TYPO3 recipe's rsync settings, so its exclude list (which has var/) +// does not apply: the language packs in var/labels are deployed with the release $rsync = get('rsync'); $rsync['exclude'] = array_merge($rsync['exclude'], [ '/{{typo3/public_dir}}/fileadmin', @@ -68,7 +56,7 @@ $rsync['exclude'] = array_merge($rsync['exclude'], [ '/package-lock.json', '/.nvmrc', ]); -// The build directory has no .gitignore filtering to do, and the filter would drop the built assets +// No .gitignore filter: it would drop the built assets $rsync['filter'] = []; // -t preserves mtimes: without it every file lands with a fresh timestamp, so unchanged // files get a new Last-Modified on every deploy and every conditional request revalidates @@ -83,124 +71,41 @@ host('prod') ->setForwardAgent(true) ->setSshMultiplexing(true) ->setDeployPath('/usr/home/eptypo3/public_html/{{application}}') - // Verify on the host once Hetzner has installed it ->set('bin/php', '/usr/bin/php85') ->set('http_user', 'eptypo3') ; -desc('Build the release locally: committed state, production dependencies, theme assets'); -task('build', function () { - // Inside DDEV directly, on the host through "ddev exec" (paths relative to the project root) - $inDdev = getenv('IS_DDEV_PROJECT') === 'true'; - $exec = fn(string $command) => runLocally($inDdev - ? $command - : 'ddev exec bash -c ' . escapeshellarg($command)); +task('deploy:build', function () { runLocally('rm -rf {{build_dir}} && mkdir -p {{build_dir}}'); runLocally('git archive HEAD | tar -x -C {{build_dir}}'); - // Assets first: Composer only publishes an extension's Resources/Public as public/_assets/ - // if the directory exists when it runs, and the theme's is created by the build - $exec('cd var/deploy-build && npm ci --no-audit --no-fund && npm run build'); - $exec('cd var/deploy-build && composer install --no-dev --prefer-dist --no-progress --no-interaction --optimize-autoloader'); - runLocally('test -L {{build_dir}}/public/_assets/' . md5('/vendor/ep/ep-theme/') . ' && test -f {{build_dir}}/packages/ep_theme/Resources/Public/entrypoints.json'); + // Assets first: Composer only publishes the theme's Resources/Public to public/_assets + // if the directory exists when it runs, and the asset build creates it + runLocally('ddev exec bash -c "cd var/deploy-build && npm ci --no-audit --no-fund && npm run build"'); + runLocally('ddev exec bash -c "cd var/deploy-build && composer install --no-dev --prefer-dist --no-progress --no-interaction --optimize-autoloader"'); })->once(); -desc('Read-only checks of the host before the first deploy (see .ddev/plans/TYPO3-13-GO-LIVE.md)'); -task('host:check', function () { - $checks = [ - 'PHP CLI 8.5.12+' => '{{bin/php}} -r \'exit(version_compare(PHP_VERSION, "8.5.12", ">=") ? 0 : 1);\' && {{bin/php}} -v | head -1', - 'PHP extensions' => '{{bin/php}} -r \'$m = array_diff(["mysqli", "pdo_mysql", "intl", "mbstring", "xml", "zip", "gd", "fileinfo", "openssl", "curl", "zlib"], get_loaded_extensions()); echo $m ? "missing: " . implode(", ", $m) : "ok"; exit($m ? 1 : 0);\'', - 'image processor (GraphicsMagick)' => 'test -x /usr/bin/gm && /usr/bin/gm version | head -1', - 'production fileadmin' => 'test -d {{fileadmin_path}} && du -sh {{fileadmin_path}} 2>/dev/null | cut -f1', - 'static/ep-reisen/fileadmin links there' => 'test "$(readlink -f /usr/home/eptypo3/public_html/static/ep-reisen/fileadmin)" = "$(readlink -f {{fileadmin_path}})" && echo yes', - 'free disk space' => 'df -h /usr/home/eptypo3 | tail -1', - 'secrets.php in place' => 'test -s {{deploy_path}}/shared/config/system/secrets.php && {{bin/php}} -r \'exit(is_array(require "{{deploy_path}}/shared/config/system/secrets.php") ? 0 : 1);\' && echo yes', - 'MyE&P reachable' => 'curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://my.ep-reisen.de/api/last-update', - ]; - $failed = false; - foreach ($checks as $label => $command) { - try { - writeln(sprintf('✔ %s: %s', $label, trim(run($command)))); - } catch (\Throwable $e) { - writeln(sprintf('✘ %s', $label)); - $failed = true; - } - } - writeln('Not checkable from the CLI: open_basedir and PHP 8.5.12 (FPM) of the static vhosts, see the go-live plan.'); - if ($failed) { - throw new \RuntimeException('Host checks failed'); - } +task('typo3:fileadmin', function () { + // Relative (-r), like Deployer's shared dirs: the vhosts answer 403 for an absolute symlink here + run('ln -sfnr {{fileadmin_path}} {{release_path}}/{{typo3/public_dir}}/fileadmin'); }); -desc('Put the secrets.php template into shared/ unless a secrets.php is there already (never overwrites)'); -task('typo3:secrets:init', function () { - $target = '{{deploy_path}}/shared/config/system/secrets.php'; - if (test("[ -s $target ]")) { - return; - } - run('mkdir -p {{deploy_path}}/shared/config/system'); - upload(__DIR__ . '/config/system/secrets.example.php', $target); - run("chmod 600 $target"); - writeln('Created ' . parse($target) . ' from the template: fill it in, then deploy again.'); -}); - -desc('Seed the shared settings.php from the live release unless it is there already (never overwrites)'); -task('typo3:settings:init', function () { - $target = '{{deploy_path}}/shared/config/system/settings.php'; - if (test("[ -s $target ]")) { - return; - } - // deploy:shared would seed it from the new release, i.e. from git, and drop production's settings - if (!test('[ -f {{deploy_path}}/current/config/system/settings.php ]')) { - return; - } - run('mkdir -p {{deploy_path}}/shared/config/system'); - run("cp -L {{deploy_path}}/current/config/system/settings.php $target"); - writeln('Created ' . parse($target) . ' from the live release.'); -}); - -desc('Stop before the database is touched while secrets.php still lacks the database connection'); -task('typo3:secrets:check', function () { - $secrets = '{{deploy_path}}/shared/config/system/secrets.php'; - $filled = run("{{bin/php}} -r '\$s = require \"$secrets\"; echo is_array(\$s) && (\$s[\"DB\"][\"Connections\"][\"Default\"][\"dbname\"] ?? \"\") !== \"\" ? \"yes\" : \"no\";'"); - if ($filled !== 'yes') { - throw new \RuntimeException(parse("$secrets has no database connection yet: fill it in, then deploy again. Nothing went live.")); - } -}); - -desc('TYPO3 - Set up all extensions (database schema: adds and changes only)'); -task('typo3:extension:setup', function () { - run('{{bin/php}} {{release_path}}/{{bin/typo3}} extension:setup'); -}); - -desc('TYPO3 - Clear all caches'); +// current_path instead of the recipe's release_path, so it also works after a rollback task('typo3:cache:flush', function () { run('{{bin/php}} {{current_path}}/{{bin/typo3}} cache:flush'); }); -desc('Link the production fileadmin into the release'); -task('typo3:fileadmin', function () { - run('test -d {{fileadmin_path}}'); - // Relative (-r), like Deployer's shared dirs: the vhosts answer 403 for an absolute symlink here - run('ln -sfnr {{fileadmin_path}} {{release_path}}/{{typo3/public_dir}}/fileadmin'); - run('test -d {{release_path}}/{{typo3/public_dir}}/fileadmin/user_upload'); -}); - -desc('Deploys TYPO3 13'); task('deploy', [ 'deploy:info', - // Build first, so a failing build aborts before the server is touched at all. - 'build', + // Build locally first, so a failing build aborts before the server is touched at all. + 'deploy:build', 'deploy:setup', 'deploy:lock', 'deploy:release', 'rsync', - 'typo3:secrets:init', - 'typo3:settings:init', 'deploy:shared', 'typo3:fileadmin', 'deploy:writable', - 'typo3:secrets:check', - // The schema has to exist before the new code starts serving traffic (adds and changes only). + // The schema has to exist before the new code starts serving traffic. 'typo3:extension:setup', // deploy:publish == deploy:symlink, deploy:unlock, deploy:cleanup, deploy:success 'deploy:publish', @@ -209,12 +114,6 @@ task('deploy', [ // Caches belong to the release that goes live, so they are cleared after the symlink flip. Hooking // into deploy:symlink rather than appending to the list above keeps them inside deploy:publish, ahead // of deploy:unlock and deploy:success - a failing cache task must not follow a declared success. -// No OPcache reset (cachetool): its web adapter would need PHP under a host's typo3temp. Vhosts that -// point at current/public keep running the previous release until PHP's path cache expires (~2 min); -// pages it renders in that window stay in the page cache until the next flush. after('deploy:symlink', 'typo3:cache:flush'); -// A rollback switches the code back; pages cached by the other release must not survive it +// Pages cached by the other release must not survive a rollback after('rollback', 'typo3:cache:flush'); - - -after('deploy:failed', 'deploy:unlock');