Configure Deployment via GitLab

To automatically deploy via gitlab we need to configure

{project_name}
| .gitlab-ci.yml
└─── deploy
   |  deploy.php
   └─── recipes
      |  rsync.php

./.gitlab-ci.yml

image: php:7.4-apache

before_script:
  - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
  - eval $(ssh-agent -s)
  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
  - mkdir -p ~/.ssh
  - chmod 700 ~/.ssh
  - ssh-keyscan -H staging.ext.dev >> ~/.ssh/known_hosts
  - apt-get update -yqq
  - apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev rsync zip libzip-dev
  - docker-php-ext-install zip
  - docker-php-ext-install gd
  - curl -sS getcomposer.org/installer | php
  - php composer.phar self-update 1.10.16
  - php composer.phar install
  - curl -LO deployer.org/deployer.phar
  - chmod +x deployer.phar

deploy_staging:
  only:
    - staging
  script:
    - php deployer.phar --file=deploy/deploy.php deploy staging

./deploy/deploy.php

Make sure to replace [domain] with the actual domain of the TYPO3 website.

<?php
namespace Deployer;

require 'recipe/common.php';
require 'recipes/rsync.php';

set('keep_releases', 5);

set('rsync', [
    'exclude' => [
        'public/fileadmin',
        'public/typo3temp',
        'docker',
        'deploy',
        '.git',
        '.gitignore',
        '.gitlab-ci.yml',
        'docker-compose.yml'
    ],
    'exclude-file' => false,
    'include' => [],
    'include-file' => false,
    'filter' => [],
    'filter-file' => false,
    'filter-perdir' => false,
    'flags' => 'rz',
    'options' => [
        'delete',
        'links'
    ],
    'timeout' => 3600,
]);

set('rsync_src', 'src/typo3');

set('bin/php', 'php74.bin.cli -d allow_url_fopen=On');


set('shared_files', [
    'public/typo3conf/AdditionalConfiguration.php'
]);

set('shared_dirs', [
    'public/fileadmin',
    'public/typo3temp',
    'public/uploads',
    'var'
]);

set('writable_dirs', [
    'public/fileadmin',
    'public/typo3temp',
    'public/uploads',
    'var'
]);

host('staging.ext.dev')
    ->stage('staging')
    ->user('root')
    ->set('http_user', 'www-data')
    ->set('writable_mode', 'chown')
    ->set('rsync_src', '.')
    ->set('rsync_dest','{{release_path}}')
    ->set('deploy_path', '/var/www/deployments/[domain].staging.ext.dev');

// Tasks...
task('deploy', [
    'deploy:prepare',
    'deploy:lock',				// lock, prevent synchrone deployments in same project
    'deploy:release',
    'rsync:warmup',				// rsync reuse previous release
    'rsync',					// rsync new code
    'deploy:shared',
    'deploy:writable',
    'deploy:clear_paths',
    'deploy:symlink',
    'deploy:unlock',
    'cleanup',
    'success'
]);

// [Optional] if deploy fails automatically unlock.
after('deploy:failed', 'deploy:unlock');

./deploy/recipes/rsync.php

This file is identical for all project - the code can simply be copy/pasted.

<?php
/* (c) HAKGER[hakger.pl] Hubert Kowalski <[email protected]>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 *
 * @contributor Steve Mueller <[email protected]>, Niklas Vosskoetter <[email protected]>
 */

namespace Deployer;

set('rsync', [
    'exclude' => [
        '.git',
        'deploy.php',
    ],
    'exclude-file' => false,
    'include' => [],
    'include-file' => false,
    'filter' => [],
    'filter-file' => false,
    'filter-perdir' => false,
    'flags' => 'rz',
    'options' => ['delete'],
    'timeout' => 300,
]);

set('rsync_src', __DIR__);
set('rsync_dest', '{{release_path}}');

set('rsync_excludes', function () {
    $config = get('rsync');
    $excludes = $config['exclude'];
    $excludeFile = $config['exclude-file'];
    $excludesRsync = '';
    foreach ($excludes as $exclude) {
        $excludesRsync.=' --exclude=' . escapeshellarg($exclude);
    }
    if (!empty($excludeFile) && file_exists($excludeFile) && is_file($excludeFile) && is_readable($excludeFile)) {
        $excludesRsync .= ' --exclude-from=' . escapeshellarg($excludeFile);
    }

    return $excludesRsync;
});

set('rsync_includes', function () {
    $config = get('rsync');
    $includes = $config['include'];
    $includeFile = $config['include-file'];
    $includesRsync = '';
    foreach ($includes as $include) {
        $includesRsync.=' --include=' . escapeshellarg($include);
    }
    if (!empty($includeFile) && file_exists($includeFile) && is_file($includeFile) && is_readable($includeFile)) {
        $includesRsync .= ' --include-from=' . escapeshellarg($includeFile);
    }

    return $includesRsync;
});

set('rsync_filter', function () {
    $config = get('rsync');
    $filters = $config['filter'];
    $filterFile = $config['filter-file'];
    $filterPerDir = $config['filter-perdir'];
    $filtersRsync = '';
    foreach ($filters as $filter) {
        $filtersRsync.=" --filter='$filter'";
    }
    if (!empty($filterFile)) {
        $filtersRsync .= " --filter='merge $filterFile'";
    }
    if (!empty($filterPerDir)) {
        $filtersRsync .= " --filter='dir-merge $filterFile'";
    }
    return $filtersRsync;
});

set('rsync_options', function () {
    $config = get('rsync');
    $options = $config['options'];
    $optionsRsync = [];
    foreach ($options as $option) {
        $optionsRsync[] = "--$option";
    }
    return implode(' ', $optionsRsync);
});


desc('Warmup remote Rsync target');
task('rsync:warmup', function() {
    $config = get('rsync');

    $source = "{{deploy_path}}/current";
    $destination = "{{deploy_path}}/release";

    if (test("[ -d $(echo $source) ]")) {
        run("rsync -{$config['flags']} {{rsync_options}}{{rsync_excludes}}{{rsync_includes}}{{rsync_filter}} $source/ $destination/");
    } else {
        writeln("<comment>No way to warmup rsync.</comment>");
    }
});


desc('Rsync local->remote');
task('rsync', function() {
    $config = get('rsync');

    $src = get('rsync_src');
    while (is_callable($src)) {
        $src = $src();
    }

    if (!trim($src)) {
        // if $src is not set here rsync is going to do a directory listing
        // exiting with code 0, since only doing a directory listing clearly
        // is not what we want to achieve we need to throw an exception
        throw new \RuntimeException('You need to specify a source path.');
    }

    $dst = get('rsync_dest');
    while (is_callable($dst)) {
        $dst = $dst();
    }

    if (!trim($dst)) {
        // if $dst is not set here we are going to sync to root
        // and even worse - depending on rsync flags and permission -
        // might end up deleting everything we have write permission to
        throw new \RuntimeException('You need to specify a destination path.');
    }

    $server = \Deployer\Task\Context::get()->getHost();
    if ($server instanceof \Deployer\Host\Localhost) {
        runLocally("rsync -{$config['flags']} {{rsync_options}}{{rsync_excludes}}{{rsync_includes}}{{rsync_filter}} '$src/' '$dst/'", $config);
        return;
    }

    $host = $server->getRealHostname();
    $port = $server->getPort() ? ' -p' . $server->getPort() : '';
    $sshArguments = $server->getSshArguments();
    $user = !$server->getUser() ? '' : $server->getUser() . '@';

    runLocally("rsync -{$config['flags']} -e 'ssh$port $sshArguments' {{rsync_options}}{{rsync_excludes}}{{rsync_includes}}{{rsync_filter}} '$src/' '$user$host:$dst/'", $config);
});

 

Configure config.yaml

Finally Make sure the file /config/sites/{sitename}/config.yaml file of the TYPO3 project has a basevariant for the staging.ext.dev hostname environment:

base: 'https://{sitename}/'
baseVariants:
  -
    base: 'http://localhost/'
    condition: 'getenv("HOSTNAME") == "localhost"'
  -
    base: 'https://{sitename}.staging.ext.dev/'
    condition: 'getenv("HOSTNAME") == "{sitename}.staging.ext.dev"'