CI setup for Sulu CMS
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