CI/CD with Drupal
Problem
- CI/CD tools exist for the popular Drupal hosting platforms
- Difficult to piece together incomplete tutorials from off the Drupal island
- Need a practical guide to self-hosted Drupal CI/CD
Prerequisites
- Drupal
- Composer
- Git
- Behat
- Command Line Bash
- Docker
- SSH
- Private-Public Keys
James
Candan /jənˈdən/
Software developer 10 yrs
From L.A., live in Chattanooga
- Developer workflow rescues
- Technology strategy
- Data visualizations
- CMS migrations
Agenda
- 10,000 ft view
- Tools
- See it in action
- Gotchas
CI/CD Process
<?php
/**
* Say what's up to the world.
*/
class BlastIt {
/**
* Outputs a simple statement.
*/
public function say_hello() {
echo "hello, world";
}
}
Code pushed to Gitlab
dev
stage
prod
Gitlab CI runs pipeline jobs on Gitlab runner
Gitlab runner spins up docker container to build, test, and deploy
Container connects to destination, fires deploy scripts
All jobs pass
CI/CD Process (cont...)
Pipelines
Jobs
# .gitlab-ci.yml
stages:
- build
- test
- deploy
. . .
my_test:
stage: test
script:
- echo "run tests"
Test
Build
Deploy
Build
Test
Deploy
What is an Artifact?
An artifact is one of many kinds of tangible by-products produced during the development of software.
- Wikipedia
Production is an Artifact of Development
Michelle Krejci
Leave out dev dependencies, build files, config files, etc
Development
Artifact
section {
height: 100px;
width: 100px;
.class-one {
height: 50px;
width: 50px;
.button {
color: #AF7AC5;
}
}
}
section {
height: 100px;
width: 100px;
}
section .class-one {
height: 50px;
width: 50px;
}
section .class-one .button {
color: #AF7AC5;
}
CI jobs produce Artifacts
A downloadable archive of the files at the end of a Job.
This is whatever you have told the job to produce.
Jobs
Tools
Docker registry
Gitlab Registry
Docker hub
SSH Task Runner
Envoy
CI Services
Gitlab CI
Build Container
Object-Oriented Thinking
JULY 16, 2014 / RODRIGO ARAÚJO
http://www.universocomputacao.com/object-oriented-thinking/
Image
Container
# Dockerfile
# Set the base image for subsequent instructions
FROM php:7.1-apache-stretch
# Update packages
RUN apt-get update
# Install PHP and composer dependencies
RUN apt-get install -qq git curl libmcrypt-dev libjpeg-dev libpng-dev libfreetype6-dev libbz2-dev unzip mysql-client
# Clear out the local repository of retrieved package files
RUN apt-get clean
# Install needed extensions
RUN docker-php-ext-install mcrypt pdo_mysql zip gd
# Install Composer
RUN curl --silent --show-error https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Install Laravel Envoy
RUN composer global require "laravel/envoy=~1.0"
Gitlab Container Registry
Docker hub
Push to Registry
# @file Envoy.blade.php
# Dockerfile
FROM php:7.1-apache-stretch
# Setup the base OS
RUN apt-get update -qq \
&& apt-get install -y --no-install-recommends build-essential \
apt-transport-https curl ca-certificates gnupg2 apt-utils
# Update packages
RUN apt-get update
# Install PHP and composer dependencies
RUN apt-get install -yqq git libmcrypt-dev mysql-client . . .
# Install needed extensions
RUN docker-php-ext-install mcrypt pdo_mysql . . .
# Install Composer
RUN curl --silent --show-error https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Install Laravel Envoy
RUN composer global require "laravel/envoy=~1.0"
Define Docker Image, Container Build, & Push
$
$ docker build -t some-name .
$
$ docker build -t registry.gitlab.com/<USERNAME>/my-project .
$ docker login registry.gitlab.com
$
$ docker build -t registry.gitlab.com/<USERNAME>/my-project .
$ docker login registry.gitlab.com
$ docker push registry.gitlab.com/<USERNAME>/my-project
$ docker build -t registry.gitlab.com/<USERNAME>/my-project .
$
An SSH task runner
Envoy
Uses Blade syntax to define tasks to be run on remote servers, such as
- cloning your project from the repository,
- installing the Composer dependencies,
- and running Drush commands.
$
$ envoy run my_first_task
# @file Envoy.blade.php
$ envoy run my_first_task
[localhost]: running first task . . .
[localhost]: hello, world
$
$ envoy run my_first_task
[localhost]: running first task . . .
[localhost]: hello, world
$ envoy run my_first_task
$ envoy run my_first_task
[localhost]: running first task . . .
[localhost]: hello, world
$ envoy run my_first_task
[localhost]: running first task . . .
[localhost]: hello, world
[localhost]: hello, world
# @file Envoy.blade.php
@servers()
@task()
@endtask
# @file Envoy.blade.php
@servers(['my_local' => 'localhost'])
@task('my_first_task', ['on' => 'my_local'])
@endtask
# @file Envoy.blade.php
@servers(['my_local' => 'localhost'])
@task('my_first_task', ['on' => 'my_local'])
echo "Running first task . . ."
@endtask
# @file Envoy.blade.php
@servers(['my_local' => 'localhost'])
@task('my_first_task', ['on' => 'my_local'])
echo "Running first task . . ."
echo "hello, world" >> hello.txt
@endtask
# @file Envoy.blade.php
@servers(['my_local' => 'localhost'])
@task('my_first_task', ['on' => 'my_local'])
echo "Running first task . . ."
echo "hello, world" >> hello.txt
cat hello.txt
@endtask
Envoy Basics
# Envoy.blade.php
@servers()
@task()
@endtask
$
Envoy Basics (cont...)
# Envoy.blade.php
@servers(['production' => 'myuser@192.168.1.1'])
@task('my_second_task', ['on' => 'production'])
@endtask
# Envoy.blade.php
@servers(['production' => 'myuser@192.168.1.1'])
@task('my_second_task', ['on' => 'production'])
echo "hello, world, again."
@endtask
# Envoy.blade.php
@setup
$name = isset($name) ? $name : 'James';
@endsetup
@servers(['production' => 'myuser@192.168.1.1'])
@task('my_second_task', ['on' => 'production'])
echo "hello, {{ $name }}"
@endtask
$ envoy run my_second_task
$ envoy run my_second_task
[production]: hello, James
$
$ envoy run my_second_task
[production]: hello, James
$ envoy run my_second_task --name=Billy
$ envoy run my_second_task
[production]: hello, James
$ envoy run my_second_task --name=Billy
[production]: hello, Billy
Deployment Strategies
- Build and rsync artifact container
- Commit to "artifact repo"
- Pull and build at destination
- Pull and build at symlink
Option 1:
Build and rsync artifact
No Example. Sorry.
Option 2:
Commit to "Artifact Repo"
Option 2: Commit to artifact repo
$ ls
$ ls
my-project
$
$ ls
my-project
$ ls my-project
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone git@gitlab.com:jcandan/my-project-artifact.git
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync my-project/ my-project-artifact/
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' my-project/ my-project-artifact/
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' --archive my-project/ my-project-artifact/
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' --archive --delete my-project/ my-project-artifact/
$
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' --archive --delete my-project/ my-project-artifact/
$ cd my-project-artifact/
$
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' --archive --delete my-project/ my-project-artifact/
$ cd my-project-artifact/
$ cat .gitignore-prod > .gitignore
$
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' --archive --delete my-project/ my-project-artifact/
$ cd my-project-artifact/
$ cat .gitignore-prod > .gitignore
$ git add .
$
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' --archive --delete my-project/ my-project-artifact/
$ cd my-project-artifact/
$ cat .gitignore-prod > .gitignore
$ git add .
$ git commit -m "Some artifact commit message."
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' --archive --delete my-project/ my-project-artifact/
$ cd my-project-artifact/
$ cat .gitignore-prod > .gitignore
$ git add .
$ git commit -m "Some artifact commit message. Maybe the job id?"
$
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' --archive --delete my-project/ my-project-artifact/
$ cd my-project-artifact/
$ cat .gitignore-prod > .gitignore
$ git add .
$ git commit -m "Some artifact commit message. Maybe the job id?"
$ git push artifact master
$ ls
my-project
$ ls my-project
LICENSE composer.json vendor
README.md load.environment.php
composer.lock web
$ git clone --origin artifact git@gitlab.com:jcandan/my-project-artifact.git
$ rsync --exclude='.git' --archive --delete my-project/ my-project-artifact/
$ cd my-project-artifact/
$ cat .gitignore-prod > .gitignore
$ git add .
$ git commit -m "Some artifact commit message. Maybe the job id?"
$ git push artifact $CI_COMMIT_REF_NAME
# Dockerfile
# Set the base image for subsequent instructions
FROM php:7.1-apache-stretch
# Add PostgreSQL 10.x Apt repository
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' >> /etc/apt/sources.list.d/pgdg.list
RUN apt-get update && apt-get install -yqq wget gnupg
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
# Install dependencies, Postgres CLI, PHP extensions, and composer
RUN apt-get update
RUN apt-get install -yqq git curl . . . postgresql-client-10 libpq-dev unzip
RUN apt-get clean
RUN docker-php-ext-install mcrypt pdo_pgsql zip gd
RUN curl --silent --show-error https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Install Laravel Envoy
RUN composer global require "laravel/envoy=~1.0"
Option 2: Commit to artifact repo
# .gitlab-ci.yml
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
services:
- postgres:10
variables:
POSTGRES_DB: drupal8
POSTGRES_PASSWORD: drupal8
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
services:
- postgres:10
variables:
POSTGRES_DB: drupal8
POSTGRES_PASSWORD: drupal8
cache:
paths:
- drush/contrib/
- vendor/
# we purposely leave out web/modules/contrib, etc.
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
# Postgres service and cache configs
# . . .
stages:
- test
- commit
- deploy
my_test:
stage: test
my_commit:
stage: commit
my_deploy:
stage: deploy
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
stages:
- test
- commit
- deploy
my_test:
stage: test
script:
- echo "run tests"
artifacts:
expire_in: 1 week
paths:
- web/
- config/
my_commit:
stage: commit
my_deploy:
stage: deploy
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
stages:
- test
- commit
- deploy
my_test:
stage: test
script:
- echo "run tests"
my_commit:
stage: commit
script:
# Some SSH key config magic and Git configurations
- cd ..
- git clone --origin artifact git@gitlab.com:<USER>/my-project-artifact.git
- rsync --archive --delete --exclude='.git' my-project/ my-project-artifact/
- cd my-project-artifact/
- cat .gitignore-prod > .gitignore
- git add .
- git commit -m "gitlab job id: $CI_JOB_ID"
- git push artifact master
my_deploy:
stage: deploy
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
stages:
- test
- commit
- deploy
my_test:
stage: test
script:
- echo "run tests"
my_commit:
stage: commit
script:
# Some SSH key config magic and Git configurations
# Commit changes to artifact repo
- git push artifact master
my_deploy:
stage: deploy
script:
- envoy run deploy # SSH task runner runs git pull and config import on production
when: manual
only:
- master
Option 2: Commit to artifact repo
Option 3:
Pull and build at destination
All on Production!
- Git pull
- Composer Install
- NPM Install
- Compile assets (Gulp)
Releases are pulled, built, and served.
Option 3: Pull and build at destination
# .gitlab-ci.yml
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
services:
- postgres:10
variables:
POSTGRES_DB: drupal8
POSTGRES_PASSWORD: drupal8
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
services:
- postgres:10
variables:
POSTGRES_DB: drupal8
POSTGRES_PASSWORD: drupal8
cache:
paths:
- drush/contrib/
- vendor/
# we purposely leave out web/modules/contrib, etc.
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
# Postgres service and cache configs
# . . .
stages:
- test
- commit
- deploy
my_test:
stage: test
my_deploy:
stage: deploy
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
stages:
- test
- commit
- deploy
my_test:
stage: test
script:
- echo "run tests"
artifacts:
expire_in: 1 week
paths:
- web/
- config/
my_deploy:
stage: deploy
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
stages:
- test
- commit
- deploy
my_test:
stage: test
script:
- echo "run tests"
deploy_dev:
stage: deploy
script:
- envoy run deploy --env=dev
only:
- develop
deploy_prod:
stage: deploy
script:
- envoy run deploy --env=prod
when: manual
only:
- master
Option 3: Pull and build at destination
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
stages:
- test
- commit
- deploy
my_test:
stage: test
script:
- echo "run tests"
deploy_dev:
stage: deploy
script:
- envoy run deploy --env=dev
only:
- develop
deploy_prod:
stage: deploy
script:
- envoy run deploy --env=prod
when: manual
only:
- master
@servers([ 'production' => 'my_user@192.168.0.11')
{{-- Assumes gitlab-runner ssh keys are on production, and composer and yarn are installed globally. --}}
@task('deploy_production', ['on' => 'production'])
@endtask
Option 3: Pull and build at destination
@servers([ 'production' => 'my_user@192.168.0.11')
{{-- Assumes gitlab-runner ssh keys are on production, and composer and yarn are installed globally. --}}
@task('deploy_production', ['on' => 'production'])
cd /var/www/html
echo 'Setting to maintenance mode.'
vendor/bin/drush state:set system.maintenance_mode 1 -y
@endtask
@servers([ 'production' => 'my_user@192.168.0.11')
{{-- Assumes gitlab-runner ssh keys are on production, and composer and yarn are installed globally. --}}
@task('deploy_production', ['on' => 'production'])
cd /var/www/html
echo 'Setting to maintenance mode.'
vendor/bin/drush state:set system.maintenance_mode 1 -y
echo 'Fetching commit changes.'
git fetch origin
git reset --hard {{ $commit }}
@endtask
@servers([ 'production' => 'my_user@192.168.0.11')
{{-- Assumes gitlab-runner ssh keys are on production, and composer and yarn are installed globally. --}}
@task('deploy_production', ['on' => 'production'])
cd /var/www/html
echo 'Setting to maintenance mode.'
vendor/bin/drush state:set system.maintenance_mode 1 -y
echo 'Fetching commit changes.'
git fetch origin
git reset --hard {{ $commit }}
echo 'Running composer install.'
composer install --no-dev --optimize-autoloader
@endtask
@servers([ 'production' => 'my_user@192.168.0.11')
{{-- Assumes gitlab-runner ssh keys are on production, and composer and yarn are installed globally. --}}
@task('deploy_production', ['on' => 'production'])
cd /var/www/html
echo 'Setting to maintenance mode.'
vendor/bin/drush state:set system.maintenance_mode 1 -y
echo 'Fetching commit changes.'
git fetch origin
git reset --hard {{ $commit }}
echo 'Running composer install.'
composer install --no-dev --optimize-autoloader
echo 'Running Drupal updates.'
vendor/bin/drush cache:rebuild
vendor/bin/drush updatedb -y
vendor/bin/drush config-split:import -y
vendor/bin/drush core:cron -y
@endtask
@servers([ 'production' => 'my_user@192.168.0.11')
{{-- Assumes gitlab-runner ssh keys are on production, and composer and yarn are installed globally. --}}
@task('deploy_production', ['on' => 'production'])
cd /var/www/html
echo 'Setting to maintenance mode.'
vendor/bin/drush state:set system.maintenance_mode 1 -y
echo 'Fetching commit changes.'
git fetch origin
git reset --hard {{ $commit }}
echo 'Running composer install.'
composer install --no-dev --optimize-autoloader
echo 'Running Drupal updates.'
vendor/bin/drush cache:rebuild
vendor/bin/drush updatedb -y
vendor/bin/drush config-split:import -y
vendor/bin/drush core:cron -y
echo "running yarn install and gulp"
cd /var/www/html/web/themes/custom/my_awesome_theme
npm install
./node_modules/gulp/bin/gulp.js
cd /var/www/html
@endtask
@servers([ 'production' => 'my_user@192.168.0.11')
{{-- Assumes gitlab-runner ssh keys are on production, and composer and yarn are installed globally. --}}
@task('deploy_production', ['on' => 'production'])
cd /var/www/html
echo 'Setting to maintenance mode.'
vendor/bin/drush state:set system.maintenance_mode 1 -y
echo 'Fetching commit changes.'
git fetch origin
git reset --hard {{ $commit }}
echo 'Running composer install.'
composer install --no-dev --optimize-autoloader
echo 'Running Drupal updates.'
vendor/bin/drush cache:rebuild
vendor/bin/drush updatedb -y
vendor/bin/drush config-split:import -y
vendor/bin/drush core:cron -y
echo "running yarn install and gulp"
cd /var/www/html/web/themes/custom/my_awesome_theme
npm install
./node_modules/gulp/bin/gulp.js
cd /var/www/html
echo 'Turning off maintenance mode.'
vendor/bin/drush state:set system.maintenance_mode 0 -y
vendor/bin/drush cache:rebuild
@endtask
Releases are cloned to tagged release directory, built, and served via symlink
12 Factor App
Option 4: Pull and build at symlink (12 Factor)
/var/www/my-app/
├── files/
├── settings.php
├── releases/
| ├── 20181225013000/
| └── 20181225023030/
| ├── .git/
| ├── composer.json/
| ├── composer.lock/
| ├── vendor/
| └── web/
| ├── core/
| ├── sites/
| | └── default/
| | ├── files -> /var/www/my-app/files/
| | └── settings.php -> /var/www/my-app/settings.php
| └── index.php
└── current -> /var/www/my-app/releases/20181225023030/
# Nginx Directory served
root /var/www/my-app/current/web
/var/www/my-app/
├── files/
├── settings.php
├── releases/
| ├── 20181225013000/
| ├── .git/
| ├── composer.json/
| ├── composer.lock/
| ├── vendor/
| └── web/
| ├── core/
| ├── sites/
| └── index.php
└── current -> /var/www/my-app/releases/20181225013000/
# Nginx Directory served
root /var/www/my-app/current/web
/var/www/my-app/
├── releases/
| ├── 20181225013000/
| ├── .git/
| ├── composer.json/
| ├── composer.lock/
| ├── vendor/
| └── web/
| ├── core/
| ├── sites/
| └── index.php
└── current -> /var/www/my-app/releases/20181225013000/
# Nginx Directory served
root /var/www/my-app/current/web
/var/www/my-app/
├── releases/
| ├── 20181225013000/
| ├── .git/
| ├── composer.json/
| ├── composer.lock/
| ├── vendor/
| └── web/
| ├── core/
| ├── sites/
| └── index.php
└── current -> /var/www/my-app/releases/20181225013000/
/var/www/my-app/
├── releases/
| ├── 20181225013000/
| ├── .git/
| ├── composer.json/
| ├── composer.lock/
| ├── vendor/
| └── web/
| ├── core/
| ├── sites/
| └── index.php
/var/www/my-app/
├── .git/
├── composer.json/
├── composer.lock/
├── vendor/
└── web/
├── core/
├── sites/
└── index.php
/var/www/my-app/
├── files/
├── settings.php
├── releases/
| ├── 20181225013000/
| ├── .git/
| ├── composer.json/
| ├── composer.lock/
| ├── vendor/
| └── web/
| ├── core/
| ├── sites/
| | └── default/
| | ├── files -> /var/www/my-app/files/
| | └── settings.php -> /var/www/my-app/settings.php
| └── index.php
└── current -> /var/www/my-app/releases/20181225013000/
# Nginx Directory served
root /var/www/my-app/current/web
Option 4: Pull and build at symlink (12 Factor)
@setup
$production_host = 'deployer@104.128.175.222';
$repository = 'git@gitlab.com:jcandan/laravel-gitlab-ci-sample.git';
$app_dir = '/var/www/laravel-gitlab-ci-sample';
$release_dir = $app_dir . '/releases';
$release = date('YmdHis');
$new_release_dir = $release_dir . '/' . $release;
@endsetup
@servers(['production' => $production_host])
@story('deploy')
clone_repository
run_composer
update_symlinks
@endstory
@task('clone_repository', ['on' => 'production'])
[ -d {{ $release_dir }} ] || mkdir {{ $release_dir }}
git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
@endtask
@task('run_composer', ['on' => 'production'])
cd {{ $new_release_dir }}
composer install --prefer-dist --no-scripts --quiet --optimize-autoloader
@endtask
@task('update_symlinks', ['on' => 'production'])
rm -rf {{ $new_release_dir }}/sites/default/files
ln -nfs {{ $app_dir }}/files {{ $new_release_dir }}/sites/default/files
ln -nfs {{ $app_dir }}/settings.php {{ $new_release_dir }}/sites/default/settings.php
ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current
@endtask
@setup
$production_host = 'deployer@104.128.175.222';
$repository = 'git@gitlab.com:jcandan/laravel-gitlab-ci-sample.git';
$app_dir = '/var/www/laravel-gitlab-ci-sample';
$release_dir = $app_dir . '/releases';
$release = date('YmdHis');
$new_release_dir = $release_dir . '/' . $release;
@endsetup
@servers(['production' => $production_host])
@story('deploy')
clone_repository
run_composer
update_symlinks
@endstory
@task('clone_repository', ['on' => 'production'])
[ -d {{ $release_dir }} ] || mkdir {{ $release_dir }}
git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
@endtask
@task('run_composer', ['on' => 'production'])
cd {{ $new_release_dir }}
composer install --prefer-dist --no-scripts --quiet --optimize-autoloader
@endtask
@setup
$production_host = 'deployer@104.128.175.222';
$repository = 'git@gitlab.com:jcandan/laravel-gitlab-ci-sample.git';
$app_dir = '/var/www/laravel-gitlab-ci-sample';
$release_dir = $app_dir . '/releases';
$release = date('YmdHis');
$new_release_dir = $release_dir . '/' . $release;
@endsetup
@servers(['production' => $production_host])
@story('deploy')
clone_repository
run_composer
update_symlinks
@endstory
@task('clone_repository', ['on' => 'production'])
[ -d {{ $release_dir }} ] || mkdir {{ $release_dir }}
git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
@endtask
@setup
$production_host = 'deployer@104.128.175.222';
$repository = 'git@gitlab.com:jcandan/laravel-gitlab-ci-sample.git';
$app_dir = '/var/www/laravel-gitlab-ci-sample';
$release_dir = $app_dir . '/releases';
$release = date('YmdHis');
$new_release_dir = $release_dir . '/' . $release;
@endsetup
@servers(['production' => $production_host])
@story('deploy')
clone_repository
run_composer
update_symlinks
@endstory
@setup
$production_host = 'deployer@104.128.175.222';
$repository = 'git@gitlab.com:jcandan/laravel-gitlab-ci-sample.git';
$app_dir = '/var/www/laravel-gitlab-ci-sample';
$release_dir = $app_dir . '/releases';
$release = date('YmdHis');
$new_release_dir = $release_dir . '/' . $release;
@endsetup
@servers(['production' => $production_host])
@setup
$production_host = 'deployer@104.128.175.222';
$repository = 'git@gitlab.com:jcandan/laravel-gitlab-ci-sample.git';
$app_dir = '/var/www/laravel-gitlab-ci-sample';
$release_dir = $app_dir . '/releases';
$release = date('YmdHis');
$new_release_dir = $release_dir . '/' . $release;
@endsetup
Provisioning Drupal
for Behat
Drupal CI Test Issues
# .gitlab-ci.yml
image: registry.gitlab.com/<USER>/my-project:latest
stages:
- test
test:
stage: test
script:
- phpunit # note: drupal does not need to be provisioned
- composer install
# Provision Drupal via site-install or database backup
- drush site:install # or pg_restore . . .
- drush config:import -y
- php -S localhost:8080 -t ./web/ &
- behat
Provisioning Options
- Restore a production database backup
- Run site install and config import
Option 1:
Restore a database backup
- setup a cron job to pull a database backup to the runner server
- setup a volume on the job's docker container
We need to get the backup to the runner job container
- volumes are configurable at gitlab-runner
- volumes are not accessible from jobs
Nope
- Push a container with all data pre-loaded
- Connect to remote database from job container
Possible work-arounds
Option 2:
Site install and config import
New in 8.6, install with existing config
$ drupal site:install --existing-config
In order to import config, the site UUID database must match configuration
Gotchas
- Standard install profile
- Override with Minimal
- Force UUID to match
hook_install()
Fails if install profile implements
$ drupal site:install --existing-config
$ drush site:install minimal
$ drush config:import
Originally Standard, try to override install profile
Fails, can't override existing install profile.
Force UUID from config
# replace UUID
$ NEW_UUID=cat config/sync/system.site.yml | grep uuid | awk '{print $2}'
$ drush config:set -y system.site uuid $NEW_UUID
Shortcut Links already set
# Delete them
$ drupal entity:delete shortcut_set default
Could not delete
field body does not exist on entity type node.
- Do not re-add field_body field via UI
- Must restore original configs for body field
Removed Body field, to implement Paragraphs
my_test:
stage: test
script:
- composer install
- export PGPASSWORD=drupal8
- vendor/bin/drush site:install standard --db-url=pgsql://postgres:drupal8@postgres:5432/drupal8 -y
- vendor/bin/drush config:set -y system.site uuid $(cat config/sync/system.site.yml | grep uuid | awk '{print $2}')
- vendor/bin/drupal entity:delete shortcut_set default
- echo "\$config['system.mail']['interface']['default'] = 'devel_mail_log';" >> web/sites/default/settings.php
- echo "\$config['system.file']['path']['temporary'] = '/tmp';" >> web/sites/default/settings.php
- echo "\$config['config_split.config_split.development']['status'] = TRUE;" >> web/sites/default/settings.php
- vendor/bin/drush en config_split -y
- vendor/bin/drush cache:rebuild
- vendor/bin/drush updatedb -y
- vendor/bin/drush config-split:import -y
- vendor/bin/drush config-split:import development -y
- vendor/bin/drush core:cron -y
- vendor/bin/drush cache:rebuild
- cd $CI_PROJECT_DIR/web/themes/custom/myawesometheme
- yarn install
- node_modules/gulp/bin/gulp.js
- cd $CI_PROJECT_DIR
- php -S localhost:8080 -t ./web/ &
- sleep 2
- 'sed -e "s/drupal_root:.*/drupal_root: \/builds\/<GITLAB USER>\/<REPO SLUG>\/web/;s/base_url:.*/base_url: http:\/\/localhost:8080/" behat.example.yml > behat.yml'
- vendor/bin/behat --colors
artifacts:
expire_in: 1 week
paths:
- web/
- config/
Tips
- Try it locally first
- Iterate locally if you can
docker run -it -v "$(PWD)":/var/www/html . . .
docker exec -it . . .
sed -e "s/drupal_root:.*/drupal_root: '\/app\/web'/;s/base_url:.*/base_url: http:\/\/appserver" > behat.yml
$ cat build.sh
lando rebuild -y
lando drush site:install standard --db-url=pgsql://drupal8:drupal8@database:5432/drupal8 -y
lando drush config:set -y system.site uuid $(cat config/sync/system.site.yml | grep uuid | awk '{print $2}')
lando drupal entity:delete shortcut_set default
# committed settings.php has lando env configs
lando drush en config_split -y
lando drush cache:rebuild
lando drush updatedb -y
lando drush config-split:import -y
lando drush config-split:import development -y
lando drush core:cron -y
lando drush cache:rebuild
# lando takes care of theme gulp
sed -e "s/drupal_root:.*/drupal_root: '\/app\/web'/;s/base_url:.*/base_url: http:\/\/appserver/" behat.example.yml > behat.yml
lando behat --colors
Q & A
Drupal + Gitlab CI
By James Candan
Drupal + Gitlab CI
- 1,140