Skip to main content

CI setup for Sulu CMS

This blog post might be outdated!
This blog post was published more than one year ago and might be outdated!
· 4 min read
Stephan Hochdörfer
Head of IT Business Operations

We started developing a few Sulu based projects. In this blog post, I want to share how we have configured our GitLab CI build pipeline to validate, build, and deploy our Sulu projects.

Our build pipeline usually consists of 3 stages:

stages:
- build
- test
- deploy

In the build stage, we install the Composer and npm dependencies - since we use a Sulu headless set up, we not only have to deal with Composer dependencies but also need to install Javascript/React dependencies for the frontend as well.

The GitLab CI job for running the PHP build looks like this. First, we validate the composer.json file to make sure the composer.lock file got updated properly. Additionally, we run the audit command which was introduced some weeks ago to detect potential security issues. And last, we run composer install to download all needed Composer packages.

We have built a custom docker image bitexpert.loc/php8.1 which contains all PHP 8.1 dependencies needed for a Symfony project. This image is used for all php-related jobs in the CI pipeline:

build:php:
stage: build
image: bitexpert.loc/php8.1
only:
- merge_requests
- tags
script:
- composer validate
- composer audit
- composer install
artifacts:
expire_in: 1h
paths:
- .composer-cache
- assets/headless/node_modules
- public/build
- var
- vendor

The GitLab CI job to install the Javascript/React dependencies is running in a similar fashion. The job is running after the build:php job to make sure the Sulu Headless bundle is installed and the needed Javascript files can be found.

build:node:
stage: build
image: node:16.15.1-slim
only:
- merge_requests
- tags
needs: ["build:php"]
script:
- cd assets/headless
- yarn
- yarn build
artifacts:
expire_in: 1h
paths:
- .composer-cache
- assets/headless/node_modules
- public/build
- var
- vendor

In the test stage, we run all our code quality checks (linting, tests, static analysis, Sonarqube). The configuration is heavily inspired by the recent blog post of Florian Engelhardt to get the most out of GitLab & GitLab CI.

In one of the jobs we check the code style for PHP:

test:php:style:
stage: test
image: bitexpert.loc/php8.1
only:
- merge_requests
- tags
script:
- vendor/bin/php-cs-fixer fix --dry-run --format=gitlab --using-cache=no > gl-cs-fixer.json

Followed by checking the code style for Javascript/React:

test:node:style:
stage: test
image: node:16.15.1-slim
only:
- merge_requests
- tags
script:
- cd assets/headless
- eslint ./src

Additionally, we run the static analysis checks for PHP:

test:php:static-analysis:
stage: test
image: bitexpert.loc/php8.1
only:
- merge_requests
- tags
script:
- vendor/bin/psalm

When running the unit tests for PHP, we generate all the files (junit.xml, coverage.xml, cobertura.xml) needed by GitLab to visualize the current state in the UI:

test:php:unittest:
stage: test
tags: [ nomad-ci ]
image: bitexpert.loc/php8.1
needs: ["build:php", "build:node"]
only:
- merge_requests
- tags
script:
- XDEBUG_MODE=coverage composer run test-coverage
coverage: '/^\s*Lines:\s*\d+.\d+\%./'
artifacts:
paths:
- coverage.xml
when: always
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: cobertura.xml

Last, in the test stage we invoke Sonarqube for some additional security and code coverage checks:

test:sonarqube-check:
stage: test
tags: [ nomad-ci ]
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
only:
- master
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- sonar-scanner
allow_failure: true

The deployment happens on our Nomad cluster. We use Terraform and the Terraform Nomad provider for triggering the deployment. The job is executed for any tag made in the master/main branch.

deploy:stage:
stage: deploy
only:
- tags
except:
- branches
environment:
name: staging
url: https://the-project.loc
script:
- cd terraform/stage
- terraform --version
- terraform init
- terraform validate
- terraform plan -out "planfile"
- terraform apply -input=false "planfile"
allow_failure: false