Pages cached while the other release was live survived the switch (seen after the rollback test: alvac served old favicon paths until a manual cache:flush). The flush task runs in current_path, which is the new release after a deploy and the previous one after a rollback. Co-Authored-By: Claude Opus 5.5 <[email protected]>
199 lines
9.0 KiB
PHP
199 lines
9.0 KiB
PHP
<?php
|
|
|
|
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
|
|
set('fileadmin_path', '/usr/home/eptypo3/public_html/ep-reisen-typo3/shared/public/fileadmin');
|
|
|
|
set('shared_dirs', [
|
|
'{{typo3/public_dir}}/typo3temp/assets',
|
|
'var/lock',
|
|
'var/log',
|
|
'var/session',
|
|
'var/spool',
|
|
// Baseline of the MyE&P import: without it every deploy would trigger a full re-import
|
|
'var/transient/ep_products_import',
|
|
]);
|
|
set('shared_files', [
|
|
'config/system/secrets.php',
|
|
]);
|
|
set('writable_dirs', [
|
|
'{{typo3/public_dir}}/typo3temp/assets',
|
|
'var/cache',
|
|
'var/lock',
|
|
'var/log',
|
|
'var/transient',
|
|
]);
|
|
set('writable_mode', 'chmod');
|
|
|
|
$rsync = get('rsync');
|
|
$rsync['exclude'] = array_merge($rsync['exclude'], [
|
|
'/{{typo3/public_dir}}/fileadmin',
|
|
'node_modules',
|
|
'/packages/*/Resources/Private/Assets',
|
|
'/.ddev',
|
|
'/deploy.php',
|
|
'/rector.php',
|
|
'/fractor.php',
|
|
'/webpack.config.js',
|
|
'/tailwind.config.js',
|
|
'/package.json',
|
|
'/package-lock.json',
|
|
'/.nvmrc',
|
|
]);
|
|
// The build directory has no .gitignore filtering to do, and the filter 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
|
|
// into a full response instead of a 304.
|
|
$rsync['flags'] = 'rzt';
|
|
$rsync['options'] = ['delete', 'links', 'chmod=D755,F664'];
|
|
set('rsync', $rsync);
|
|
|
|
host('prod')
|
|
->setHostname('dedi10193.your-server.de')
|
|
->setRemoteUser('eptypo3')
|
|
->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));
|
|
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/<hash>
|
|
// 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');
|
|
})->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('<info>✔</info> %s: %s', $label, trim(run($command))));
|
|
} catch (\Throwable $e) {
|
|
writeln(sprintf('<error>✘</error> %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');
|
|
}
|
|
});
|
|
|
|
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('<comment>Created ' . parse($target) . ' from the template: fill it in, then deploy again.</comment>');
|
|
});
|
|
|
|
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');
|
|
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',
|
|
'deploy:setup',
|
|
'deploy:lock',
|
|
'deploy:release',
|
|
'rsync',
|
|
'typo3:secrets: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).
|
|
'typo3:extension:setup',
|
|
// deploy:publish == deploy:symlink, deploy:unlock, deploy:cleanup, deploy:success
|
|
'deploy:publish',
|
|
]);
|
|
|
|
// 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 www.ep-reisen.de/typo3temp, which
|
|
// is the static docroot. Not needed either: the front controller stubs in the static docroots resolve
|
|
// "current" with readlink() on every request, so a new release is always loaded under new paths.
|
|
after('deploy:symlink', 'typo3:cache:flush');
|
|
// A rollback switches the code back; pages cached by the other release must not survive it
|
|
after('rollback', 'typo3:cache:flush');
|
|
|
|
after('deploy:failed', 'deploy:unlock');
|