
CI/CD using gitlab with Angular

Syed Saad Qamar
Software Engineer
/SyedSaadQamar96
/in/syed-saad-qamar
/saadqamar01
/saadqamar96




Road map of today's dev session
- Set up a sample app with e2e and unit testing
- Configure this for prod on every git tag
- Configure this for stage on every commit
- Configure gitlab-ci to do unit tests, e2e test and deploy to surge

Build the sample app
# run this in the terminal
npm install -g @angular/cli
ng new gitlab-ci-cd-demo
cd gitlab-ci-cd-demo
ng e2e
ng test --watch false
ng serve -o
app.component.html
<h1>{{title}}</h1>
<div>
Points: <span>{{points}}</span>
</div>
<button (click)="plus1()">Plus 1</button>
<button (click)="reset()">Reset</button>
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'Gitlab ci/cd with angular';
points = 1;
plus1() {
this.points++;
}
reset() {
this.points = 0;
}
}
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'Gitlab ci with angular'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('Gitlab ci with angular');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Gitlab ci with angular');
}));
it('should increase points by 1 if button clicked', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.componentInstance.points).toBe(1);
fixture.debugElement.nativeElement.querySelector('button').click();
expect(fixture.componentInstance.points).toBe(2);
}));
});
app.component.spec.ts
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get('/');
}
getTitleText() {
return element(by.css('h1')).getText();
}
getPoints() {
return element(by.cssContainingText('div', 'Points')).$('span').getText();
}
getPlus1Button() {
return element(by.cssContainingText('button', 'Plus 1'));
}
getResetButton() {
return element(by.cssContainingText('button', 'Reset'));
}
}
app.po.ts
app.e2e-spec.ts
import { AppPage } from './app.po';
describe('App Page', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display Gitlab ci with angular', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('Gitlab ci with angular');
});
it('should click three times and reset with matching points', () => {
page.navigateTo();
expect(page.getPoints()).toBe('1');
page.getPlus1Button().click();
page.getPlus1Button().click();
page.getPlus1Button().click();
expect(page.getPoints()).toBe('4');
page.getResetButton().click();
expect(page.getPoints()).toBe('0');
});
});
karma.conf.js
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
process.env.CHROME_BIN = require('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'),
require('karma-spec-reporter')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml', 'spec'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome_no_sandbox'],
customLaunchers: {
Chrome_no_sandbox: {
base: 'ChromeHeadless',
browserDisconnectTolerance: 3,
browserNoActivityTimeout: 300000,
browserDisconnectTimeout: 300000,
flags: [
'--disable-web-security',
'--disable-gpu',
'--no-sandbox'
],
}
},
timeoutInterval: 300000,
browserNoActivityTimeout: 300000,
browserDisconnectTimeout: 300000,
concurrency: Infinity,
singleRun: true
});
};
protractor-ci.conf.js
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
chromeOptions: {
args: [
'--headless',
'--disable-gpu',
'--no-sandbox',
'--disable-extensions',
'--disable-dev-shm-usage',
'--disable-browser-side-navigation'
]
},
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.e2e.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};
package.json
{
"name": "gitlab-ci-demo",
"version": "0.0.7",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"build-prod": "./node_modules/@angular/cli/bin/ng build --progress false --prod --base-href gitlab-ci-demo.surge.sh",
"test-ci": "ng test --no-watch --no-progress",
"e2e-ci": "ng e2e --protractor-config=e2e/protractor-ci.conf.js",
"deploy": "npm run build && mv dist/index.html dist/200.html && surge build"
},
"private": true,
"dependencies": {
"@angular/animations": "~8.2.14",
"@angular/common": "~8.2.14",
"@angular/compiler": "~8.2.14",
"@angular/core": "~8.2.14",
"@angular/forms": "~8.2.14",
"@angular/platform-browser": "~8.2.14",
"@angular/platform-browser-dynamic": "~8.2.14",
"@angular/router": "~8.2.14",
"rxjs": "~6.4.0",
"tslib": "^1.10.0",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.803.21",
"@angular/cli": "~8.3.21",
"@angular/compiler-cli": "~8.2.14",
"@angular/language-service": "~8.2.14",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "^5.0.0",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
"puppeteer": "~1.17.0",
"karma-chrome-launcher": "^3.1.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"karma-spec-reporter": "0.0.32",
"protractor": "~5.4.0",
"surge": "^0.21.3",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.5.3"
}
}
Build and deploy from your local system
- Let’s install surge for deployment:
npm install surge --save-dev
- If you use surge for the first time let’s login:
node_modules/.bin/surge login
ng build --prod
-
Now let’s build our angular app (built code will go into dist/ folder):
- And now let’s deploy manually: (replace XXX with whatever you like)
./node_modules/.bin/surge -p dist/<your-project-name>/ — domain XXX-stage.surge.sh
Ex: ./node_modules/.bin/surge -p dist/gitlab-ci-demo/ --domain gitlab-ci-demo-stage.surge.sh
Let’s configure the gitlab-ci stage deployment
- Create a GitLab project: https://gitlab.com/projects/new
- Add gitlab as remote and push your local repo including the changes
git remote add origin git@gitlab.com:XXXuserXXX/XXXprojectXXX.git
git add .
git commit -m "First commit"
git push -u origin master
- Now we need the SURGE_LOGIN and SURGE_TOKEN to do the deployment
cat ~/.netrc
- Create two pipeline environment variables in settings of your gitlab project:
https://gitlab.com/<user.name>/<project.name>/-/settings/ci_cd
Ex: https://gitlab.com/syed.saad1/gitlab-ci-demo/-/settings/ci_cd


- Add the variable SURGE_LOGIN (login string from ~/.netrc) and SURGE_TOKEN (password string from ~/.netrc).
image: node:10.14.2
cache:
paths:
- node_modules/
before_script:
- apt-get update && apt-get install -y gnupg2
# Add Google Chrome to aptitude's (package manager) sources
- echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee -a /etc/apt/sources.list
# Fetch Chrome's PGP keys for secure installation
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
# Update aptitude's package sources
- apt-get -qq update -y --allow-unauthenticated
# Install latest Chrome stable, Xvfb packages
- apt-get -qq install -y --allow-unauthenticated google-chrome-stable xvfb gtk2-engines-pixbuf xfonts-cyrillic xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable imagemagick x11-apps default-jre
# Launch Xvfb
- Xvfb :0 -ac -screen 0 1024x768x24 &
# Export display for Chrome
- export DISPLAY=:99
- apt-get update && apt-get install -y unzip fontconfig locales 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
- google-chrome --version
deploy_stage:
stage: deploy
environment: Stage
only:
- master
script:
- rm ./package-lock.json
- npm install
- npm run test-ci
- npm run e2e-ci
- ./node_modules/@angular/cli/bin/ng build --progress false --prod --base-href gitlab-ci-demo-stage.surge.sh
- ./node_modules/.bin/surge -p dist/gitlab-ci-demo/ --domain gitlab-ci-demo-stage.surge.sh
- Create .gitlab-ci.yml doing all the nice things:
- Add and commit the gitlab file and go to your projects gitlab piplines to see if it works

- Open the domain you defined ie.: gitlab-ci-demo-stage.surge.sh
- Now we need a separate ci/cd for production. But this time we only want to run it for git tags
deploy_production:
stage: deploy
environment: Production
only:
- tags
script:
- rm ./package-lock.json
- npm install
- npm run test-ci
- npm run e2e-ci
- ./node_modules/@angular/cli/bin/ng build --progress false --prod --base-href gitlab-ci-demo.surge.sh
- ./node_modules/.bin/surge -p dist/gitlab-ci-demo/ --domain gitlab-ci-demo.surge.sh
- To deploy on prod you need to add a tag:
git add .gitlab-ci.yml
git commit -m 'adds prod to gitlab-ci'
git tag v1.0.23 -am 'quick to the golden submarine'
git push origin v1.0.23
- Go to your pipelines and see how prod is deployed based on your tag.
- Go to your prod domain on xxx-prod.surge.sh an see how you just deployed automatically based on git tags ;-)


CI/CD using gitlab with Angular
By Syed Saad Qamar
CI/CD using gitlab with Angular
- 268