GitLab CI/CD with Angular 7 & Firebase
Automate build, test and deployment using GitLab CI/CD to Firebase hosting.
I have recently found a way how to configure continuous deployment of your GitLab repository into Firebase hosting.
Prerequisites
1. GitLab Account
2. Firebase Account
Overview
1. Create a new Angular 7 Project
2. Create a new GitLab Repository
3. Configure the GitLab Repository on our project
4. Configure the Angular 7 project for CI/CD (Build, Test, Deploy)
5. Create a Firebase project
6. Configure CI/CD on GitLab
First of all, create a new angular 7 project using angular CLI. Use the following CLI command to create a new project.
ng new angular_seven_firebase_ci_cd
Next, create a new GitLab repository.
Next, run the following commands on your terminal window
cd angular_seven_firbase_ci_cd
git init
git remote add origin https://gitlab.com/##YOUR_USER_NAME##/angular_seven_firebase_ci_cd.git
git add .
git commit -m "Initial commit"
git push -u origin master
Then, configuration Angular 7 project for Build, Test and Deployment.
Angular 7 comes with 2 test tools: Karma for unit tests and Protractor for end-2-end or integration tests. Both approaches have in common, that they open a browser to execute the tests.
During development we can run Karma based tests using the command ng test which opens a browser (Chrome), executes tests and re-runs all tests whenever code is changed. To execute Protractor based tests we use the command ng e2e which also opens a browser and runs the tests.
On our CI environment, we only need to execute our test cases without the browser’s GUI and memory overhead. With the headless mode of chrome, this is possible.
Puppeteer
Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default but can be configured to run full (non-headless) Chrome or Chromium.
First, we need to include a browser in our CI environment. We will use the node library Puppeteer, which bundles chrome. With that, we can make sure, that we have the right browser whether we are on our development or on our CI environment.
npm install --save-dev puppeteer
Karma Configuration
open your karma.conf.js file from your project src folder. We add a custom launcher in our karma.conf.js to start Chrome in a headless mode in our CI environment. Additionally, we need to use the option — no-sandbox, to make it work, as we made no additional user configuration on our docker image.
Your karma.conf.js file should be like this
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const puppeteer = require('puppeteer');
process.env.CHROME_BIN = puppeteer.executablePath();
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},
singleRun: false
});
};
Protractor Configuration
create a new file on the e2e folder with the name protractor-ci.conf.js
For e2e tests we add the following configuration on that file, to be able to call chrome in headless mode with the option — no-sandbox.
your protractor-ci.conf.js file will be like this
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const config = require('./protractor.conf').config;
const puppeteer = require('puppeteer');
config.capabilities = {
browserName: 'chrome',
chromeOptions: {
args: ['--headless', '--no-sandbox'],
binary: puppeteer.executablePath()
}
};
exports.config = config;
Add Node Scripts for CI/CD
To build, test and deploy properly on CI, we add the following scripts into our package.json
configuration file.
your package.json like this
{
"name": "angular_seven_firebase_ci_cd",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"build-prod": "ng build --prod",
"test-ci": "ng test --no-watch --no-progress --browsers=ChromeHeadlessNoSandbox",
"e2e-ci": "ng e2e --protractor-config=e2e/protractor-ci.conf.js",
"deploy": "firebase deploy --token $FIREBASE_TOKEN --non-interactive"
},
"private": true,
"dependencies": {
"@angular/animations": "~7.1.0",
"@angular/common": "~7.1.0",
"@angular/compiler": "~7.1.0",
"@angular/core": "~7.1.0",
"@angular/forms": "~7.1.0",
"@angular/platform-browser": "~7.1.0",
"@angular/platform-browser-dynamic": "~7.1.0",
"@angular/router": "~7.1.0",
"core-js": "^2.5.4",
"rxjs": "~6.3.3",
"tslib": "^1.9.0",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.11.0",
"@angular/cli": "~7.1.0",
"@angular/compiler-cli": "~7.1.0",
"@angular/language-service": "~7.1.0",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~4.5.0",
"firebase-tools": "^6.2.2",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.1.1",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.4.0",
"puppeteer": "^1.11.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.1.6"
}
}
Commit to GitLab
Now we can share our local code with the remote repository in GitLab. The code should be visible in your GitLab repository now.
Create Firebase Application
Next, create a new project in fire base.
Install Firebase Tools on our Angular 7 Project
As we want to deploy to firebase we need the firebase tools. Let’s add them to our dev-dependencies and also install them globally on our machine:
npm install --save-dev firebase-tools
npm install -g firebase-tools
Now the firebase configuration file automatically created on project root folder
Next, we push all changes now to our master branch.
Setup & Configure CI/CD in GitLab
GitLab CI/CD is configured with a special file called .gitlab-ci.yml, which needs to be placed in the root folder of the project. GitLab automatically detects the file and runs the pipeline according to the configuration. Let’s add the following file to get started with a simple 3 step pipeline, which includes build, test and deployment to production with caching and storing of build artefacts.
Your .gitlab-ci.yml file like the following
image: node:latest
build:
stage: build
cache:
paths:
- node_modules/
script:
- npm install --quiet
- npm run build-prod
artifacts:
paths:
- dist/
test:
stage: test
cache:
policy: pull
paths:
- node_modules/
script:
# install dependencies to use chrome w/ puppeteer
- apt update && apt install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
- npm run test-ci
- npm run e2e-ci
deploy_prod:
stage: deploy
environment:
name: production
url: $FIREBASE_URL
only:
- master
dependencies:
- build
cache:
policy: pull
paths:
- node_modules/
script:
- npm run deploy
Explanation of the Configured Pipeline
With image: node:latest
we tell our pipeline which docker container should be used for the build. For Angular 7 we require Node latest version.
We separated the pipeline into 3 jobs: build
, test
and deploy_prod
. Each step is executed independently on the image we defined, one after another. The pipeline stops whenever an error occurs. Example: If build
fails, test
and deploy_prod
will not be executed.
With caches: path: node_modules/
we cache all node_modules which are downloaded during the installation of dependencies. This will speed up the subsequent jobs of the pipeline as they are all executed on a blank image. The default cache-policy is pull-push
. That means, the files which are defined to be cached, get pulled from the cache repository before any script runs within the job. After all scripts run, the files are pushed back to the cache repository.
For the test
and deploy_prod
job we define the same folder to be cached but with the cache-policy pull
, because we do not need to write the cache back. This speeds up the process.
With the command only: master
we tell the deploy_prod
job to only run, if changes on the master branch occur. With that, we make sure, that not every change on feature or other branches gets deployed to production.
With the command dependencies: build
we tell the deploy_prod
job to fetch those artifacts which we defined in the build job. This way we do not need to build it again and make sure that we do not deploy something else.
To track deployments to the prod environment we use the command environment
which tells GitLab to consider this Job as a production deployment. Find more information here on GitLab Environments.
The main part of each job is the script:
section which has the following tasks:
- Build: Install all required modules which are described in
package.json
and build the artifacts which we want to deploy. - Test: Install required libraries to run chrome on the docker image as discussed in puppeteer’s troubleshooting documentation. Then execute Karma and Protractor tests.
- Deploy: Deploy artifacts to Firebase.
If any command fails, the whole pipeline fails. That means, a failed test will prevent from deploying to the server (which is good as we do not want to break the production).
Set environment variables in GitLab
Goto your project settings on GitLab. Choose the CI/CD submenu. Expand the variable section and add the following two variables.
FIREBASE_TOKEN
, which is used to auth GitLab against Firebase andFIREBASE_URL
, which is the URL of our project and only used as a meta information for our environment configuration in GitLab.
Use the following command to get the Firebase Token from your terminal window
firebase login:ci
It’s prompts a login window. You have logged in successfully into your firebase account you get the token. copy that token from your terminal window and paste it here.
Firebase url is our firebase app domain url. you can find that on your firebase console.
Now commit any change to master or merge to master, and you will see a deployment to Firebase. If you commit to any other branch, only the build and test jobs will run.
You can find your application using your firebase domain url