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

  • 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