How to setup your Ionic project with automated Continuous Integration and Deployment with Gitlab CI

January 08, 2019

How useful would it be to have all your unit and end-to-end integration test run every time you commit your code? For those times you didn’t think it was necassary for the change, or just simply forgot to before pushing the commit.

A automated continuous integration build system has been a cornerstone of good software development practices, even before DevOps came on the scene.

Here I’m going to show you how to setup your own continuous integration and deployment pipeline using the GitLab CI service for your Ionic project.

First lets create a new Ionic v4 project. Make sure you have Ionic CLI tools installed by running:
npm install -g ionic

Lets create an default Ionic v4 starter project using the tabs layout:
ionic start –type angular myProject tabs

First lets test running the build, lint, and test commands directly from the command line
cd myProject
npm run build
npm run lint
npm run test
npm run e2e

You’ll notice the npm run test command doesn’t exit, it watches for any changes and then run the tests again.

In a continuous build environment we don’t want this behaviour, but instead want it to run the tests once, and have the command fail if any test fail.

So lets update the package.json file to add a new test script

"test-ci": "ng test --watch=false"

Run npm run test-ci and you’ll see it exits after executing all the tests once.

Now we’re ready to start on our GitLab CI integration. First create a project on GitLab.com and initialise your Ionic project folder with the git repository using the commands on the GitLab project page. Commit the code of the generated Ionic project and push it.

You can follow along this tutorial with the repository at https://gitlab.com/apporchestra/ionic-firebase-gitlab-ci-cd/ which has the commits for each step in the tutorial.

I won’t cover all the possibilities of the the CI configuration file, you can check the documentation at https://docs.gitlab.com/ee/ci/quick_start/ for developing more complex build scripts. Here we will just create a basic build pipeline with a single stage.

In the project root folder create the file .gitlab-ci.yaml with the following contents:

# https://gitlab.com/ci/lint
# This image is based from node and includes what we need to run tests with Chrome headless
image: weboaks/node-karma-protractor-chrome

variables:

stages:
  - build-test

build-test:
  stage: build-test
  script:
    - npm install
    - npm run build
    - npm run test-ci
    - npm run e2e

You’ll notice the first configuration line image: weboaks/node-karma-protractor-chrome This specifies that the build runner will use that particular Docker image, which already contains the programs we need to build the app and run the tests. In particular Node.js and a recent version of Chrome which supports running in a headless mode.

If you commit and push that file then GitLab will kick off a build using one of their public runners, which you get 2000 free build minutes a month with.

But alas you will find some errors in the build log when it attempts to execute the tests.

Cancel the running build as it will otherwise timeout after an hour, wasting your free build minutes.

To fix this we’re going to need to change the options passed to Chrome from Karma so it can run in the headless Docker execution environment. In karma.config.js update the browsers property be

    // When running in a continuous integration environment configure Chrome to run headless
    browsers: [process.env.CI ? 'ChromeCI' : 'Chrome'],
    customLaunchers: {
      ChromeCI: {
        base: 'ChromiumHeadless',
        flags: [
          '--no-sandbox', // required to run without privileges in docker
          '--disable-setuid-sandbox',
          '--disable-gpu',
          '--disable-software-rasterizer' // https://stackoverflow.com/questions/50143413/errorgpu-process-transport-factory-cc1007-lost-ui-shared-context-while-ini
        ]
      }
    },

Commit and push the changes the Karma configuration. Next you’ll notice the Protractor end-to-end test fails with Chrome errors, so we’ll need to update the e2e/protractor.conf.js file similarly.

First add:

// In the GitLab continuous integration (CI) environment we need to run Chrome headless
const chromeArgs = !!process.env.CI ?
  ['--test-type', '--headless', '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-gpu',
    '--disable-software-rasterizer' // https://stackoverflow.com/questions/50143413/errorgpu-process-transport-factory-cc1007-lost-ui-shared-context-while-ini],
  ] :
  ['--test-type']

then update the capabilities property to be:

  capabilities: {
    'browserName': 'chrome',
    chromeOptions: {
      args: chromeArgs
    }
  },

Commit and push the changes, and we’ll find yet another error in the build logs.

In the documentation for the docker file we are using there is a section about the headless mode. There is a particular tagged version of the Docker image we can use for headless support.

So in the .gitlab-ci.yaml file we can update the image value to weboaks/node-karma-protractor-chrome:headless

The docker image can change over time if we use the base labels, or no label. So we don’t have a perfectly reproducible build setup. Changes to Chrome or Node.js in the Docker image could impact on the build.

If we want to fix the build environment further we can use the labels which specify a git commit hash of the Docker configuration.

The documentation for the Docker image we are using states:

pinned tags
headless, alpine, debian-node* and alpine-node* are automated build, rebuild each time a commit is pushed to this repo and each time an image is pushed to official node image. These tags are always up to date, but can break builds with a new node version or a new chromium versions.

debian-<commit-hash> and alpine-<commit-hash> tags are available to target a specific commit, they are never rebuilt and will have the same node and chromium version forever.

Push the change with the headless label and now our CI build will succeed!

The next step is to update our CI script to add support for continuous deployment to Firebase hosting. If the unit and e2e tests pass, then the build script will continue to the deploy command.

Before we do that you’ll need to create a new project from the Firebase console at https://console.firebase.google.com/ and take note of the project number.

Add the firebase tools as a dev dependency so we can run the deploy command by running:
npm add firebase-tools –save-dev

Let’s just manually create the firebase.json file that the Firebase tool would produce from initialising the project, and set the hosting contents folder to be that of the Ionic build output, i.e. www.

{
  "hosting": {
    "public": "www"
  }
}

And add an NPM script to package.json to run the deploy command, where is your Firebase project id.

"deploy": "npx firebase deploy -P <project-id>"

If you haven’t seen the npx command before, its a nicer cross-platform way to execute a command that has existed since npm v5.2.0

Typically before you would have executed it using the full path the command, i.e. ./node_modules/.bin/firebase

By default, npx will check whether the command exists in $PATH, or in the local project binaries, and execute that. If the command isn’t found, it will even be installed prior to executing.

Next add npm run deploy to the .gitlab-ci.yaml build script, so now the build-test stage is looking like

build-test:
  stage: build-test
  script:
    - npm install
    - npm run build
    - npm run test-ci
    - npm run e2e
    - npm run deploy

Finally for the firebase command to work we need to provide a means for it to authenticate. This can be done by setting the FIREBASE_TOKEN environment variable in the GitLab CI runner.

Follow the instructions at https://github.com/firebase/firebase-tools#using-with-ci-systems to generate the token. Then in your project in GitLab.com go to CI/CD -> Settings -> Environment Variables and create the FIREBASE_TOKEN variable with the generated token.



Now we’re all ready to go! Push the changes and you should have your app built and deployed to Firebase Hosting.



That completes the basic build and deploy pipeline, which as you’ve seen even with that there’s a little bit of work getting all the configuration correct.

The possibilities to develop a build/deploy pipeline further can include functionality such as:

  • Build and deploy a Google App Engine project
  • Build and deploy Firebase Cloud functions and other Firebase configuration (storage and database rules)
  • Execute integration tests covering App Engine and Firebase functionality
  • Native Android builds
  • Deployment of Android builds to the Play store
  • Notifications when new builds have been completed/deployed
  • Integrate multi-environment support throughout the project and have GitOps style deployment to QA and production from named branches
  • Handle special tokens in Git commit messages to alter the build process
  • Deploy a canary build to App Engine, and run some smoke tests on it before promoting it to the default version.

All this takes a considerable amount of time to build, but we’ve already done most of it in our brand new complete Ionic starter project. If you want to save yourself weeks or even months or work engineering a solid foundation to your app and cloud project then take a look at Lattice.