Continuous Integration (CI) on GitLab provides a project with a way to run a build process on every commit, so that instant feedback is given whether the master branch of the project still builds correctly. In addition, the build process can create artifacts with can form the basis for releases.

A git project can be cloned and built by any developer on their local machine, but this may require many dependencies and setting up a development machine can be a somewhat long process. End users wanting to run the project on their own server should not need to go through the entire build process. To avoid this, we can make regular pre-built releases available that end users can download and install on their server.

Furthermore, Continuous Deployment (CD) can be used to (build and) deploy the project to its main server whenever the repository changes.

Implementation

To make GitLab’s CI/CD feature work, a CI/CD configuration file must be present at the project root: gitlab-ci.yml. This file contains the stages involved in the build, test, and development process. In this example, the project only has a build stage.

GitLab’s build process works by settings up a virtual machine from a Docker image. It’s possible to provide a custom Docker image, but several pre-built Docker images exist that we can leverage to save time. One of these is tetraweb/php, which defines a machine with NodeJS and PHP installed (plus Composer). It provides several PHP versions, the latest being 7.1 at this time. In our configuration file, we instruct GitLab to use this Docker image:

# The "tetraweb/php" image has both PHP And NodeJS installed,
# so we can use it rather than creating our own docker image.
image: tetraweb/php:7.1

Although the tetraweb/php Docker image has all PHP modules available, its PHP is not configured to use all of them. In particular, the php-spreadsheet dependency (why I happen to use) needs the GD extension. We could ignore these dependencies by running composer --ignore-platform-reqs, or we can install them (which we will do). We’ll add a bash script that configures our Docker image further:

#!/bin/bash

# We need to install dependencies only for Docker
[[ ! -e /.dockerenv ]] && exit 0

set -xe

# Enable extensions zip (for Composer) and gd (for php-spreadsheet).
docker-php-ext-enable zip gd

# Install prestissimo extension, which enables composer 
# to do its downloads in parallel.
composer global require hirak/prestissimo

This script lives at ci/docker-install.sh. It enables PHP extensions zip (needed by Composer) and gd. Furthermore, it installs Composer’s prestissimo extension, which will speed up Composer dependency installation by allowing parallel downloads, making the CI process faster.

In our gitlab-ci.yml configuration file, we’ll have GitLab’s CI/CD run our Docker install scripts before any job it does:

# This gets executed before any job runs. Here, we run a docker
# install script that enables required PHP extensions.
before_script:
  - bash ci/docker_install.sh

The main dependencies for executing CI/CD processes are now set up. We can now define a stage in the configuration file, which we’ll call build. It’s possible to define multiple stages, and we may add a test and deploy stage later on.

stages:
  - build

By itself, a stage does nothing. It merely groups a number of jobs. We’ll add two jobs: one two build the PHP back-end, and one to build the JavaScript front-end.

For the front end, we’ll have npm install all project dependencies, then build the project. This will use Webpack to generate a distribution in the /dist directory.

build-frontend:
  stage: build
  script:
    - npm install
    - npm run build-dev

In order to facilitate installation on a server, we’ll need to make the result of the build available somehow. This can be done by saving a build artifact, which users can download from the CD/CI pipeline result on GitLab:

build-frontend:
  stage: build
  script:
    - npm install
    - npm run build-dev
  artifacts:
    paths:
      - dist
    expire_in: 1 week    

The back-end is built in a similar way. We enter into the api directory and use Composer to install all PHP dependencies.

# This job builds the back-end. It uses composer to
# install all PHP project dependencies, then creates
# an artifact containing the full api directory.
build-backend:
  stage: build
  script:
    - cd api
    - composer install
    - cd ..
  artifacts:
    paths:
      - api
    expire_in: 1 week