DEV Community

Cover image for Building a Rock-Solid Angular CI/CD Pipeline with GitLab: A Step-by-Step Guide
Ángel Quiroz
Ángel Quiroz

Posted on

Building a Rock-Solid Angular CI/CD Pipeline with GitLab: A Step-by-Step Guide

GitLab CI/CD is a powerful tool that can be used to automate the build, test, and deployment of software applications. With GitLab CI/CD, you can define a set of rules that govern how your application is built, tested, and deployed, and then let GitLab take care of the rest.

In this tutorial, we will be looking at a GitLab CI/CD pipeline configuration file and explaining what each section does.

Let's start by taking a look at the default section:

default:
  interruptible: true
  image: node:lts-alpine
Enter fullscreen mode Exit fullscreen mode

This section defines the default settings for the entire pipeline. In this case, we have set the interruptible flag to true, which means that the pipeline can be interrupted by a user or by another job. We have also set the image to node:lts-alpine, which means that each job in the pipeline will be run in a Docker container based on the node:lts-alpine image.

Next, we have the stages section:

stages:
  - dependencies
  - quality
  - assemble
  - deploy
Enter fullscreen mode Exit fullscreen mode

This section defines the stages that the pipeline will go through. Each stage represents a different step in the process of building, testing, and deploying the application. In this case, we have defined four stages: dependencies, quality, assemble, and deploy.

Now, let's look at the install job:

install:
  stage: dependencies
  script:
    # Install dependencies
    - npm install --prefer-offline
  cache:
    key:
      files:
        - package.json
    paths:
      - node_modules
Enter fullscreen mode Exit fullscreen mode

This job is part of the dependencies stage and is responsible for installing the application's dependencies. We use the npm command to install the dependencies and use the --prefer-offline flag to ensure that cached versions of dependencies are used if available. We also define a cache for the node_modules directory based on the package.json file.

Next, we have the lint job:

lint:
  stage: quality
  needs: ["install"]
  script:
    - npm run lint
  cache:
    key:
      files:
        - package.json
    paths:
      - node_modules
    policy: pull
Enter fullscreen mode Exit fullscreen mode

This job is part of the quality stage and is responsible for running the application's linter. We define a dependency on the install job using the needs keyword. We use the npm run lint command to run the linter and define a cache for the node_modules directory based on the package.json file. We also set the cache policy to pull, which means that GitLab will attempt to pull the cache from a remote cache server before running the job.

Next, we have the test job:

test:
  stage: quality
  needs: ["install"]
  before_script:
    # Download Chrome
    - apk add chromium
    - export CHROME_BIN=/usr/bin/chromium-browser
    - export CHROME_PATH=/usr/lib/chromium/
    # Download Firefox
    - apk add firefox-esr
    - export FIREFOX_BIN=/usr/bin/firefox
    - export FIREFOX_PATH=/usr/lib/firefox/
    # X Server
    - apk add xvfb
    - export DISPLAY=:99
    - Xvfb :99 -screen 0 1024x768x24 > /dev/null

script:
    - npm run test -- --browsers=Headless_Chrome --no-watch
Enter fullscreen mode Exit fullscreen mode

This was the stage that was the most difficult to solve, this is responsible for running automated tests for the project. This stage has a before_script section where we download and configure web browsers (Chrome and Firefox) as well as an X Server for headless testing. The script section runs the automated tests using the npm run test command with the --browsers=Headless_Chrome --no-watch options. The cache configuration is similar to the previous stages.

The config for the Headless browser is like

// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html

module.exports = function (config) {
  config.set({
    ...
    browsers: ["Chrome"],
    customLaunchers: {
      Headless_Chrome: {
        base: "Chrome",
        flags: ["--no-sandbox", "--disable-gpu"],
      },
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

The assemble stage is responsible for building the project. It depends on the test and lint stages and has a script section that runs the npm run build command. The artifacts section specifies that the resulting dist directory should be stored as a build artifact. The cache configuration is similar to the previous stages.

Finally, we have the deploy stage, which is responsible for deploying the project. It depends on the assemble stage and has a variables section that sets the NODE_ENV environment variable to "production". The before_script section simply echoes the name of the environment that we're deploying to.

We also have two different deploy-* jobs that extend the deploy stage with specific configuration for different environments. These jobs are triggered manually and have a variables section that sets the NODE_ENV environment variable to "development" or "staging". The script section for each of these jobs echoes the name of the environment that we're deploying to and runs any additional deployment actions that are required.

Complete file

default:
  interruptible: true
  image: node:lts-alpine

stages:
  - dependencies
  - quality
  - assemble
  - deploy

install:
  stage: dependencies
  script:
    # Install dependencies
    - npm install --prefer-offline
  cache:
    key:
      files:
        - package.json
    paths:
      - node_modules

lint:
  stage: quality
  needs: ["install"]
  script:
    - npm run lint
  cache:
    key:
      files:
        - package.json
    paths:
      - node_modules
    policy: pull

test:
  stage: quality
  needs: ["install"]
  before_script:
    # Download Chrome
    - apk add chromium
    - export CHROME_BIN=/usr/bin/chromium-browser
    - export CHROME_PATH=/usr/lib/chromium/
    # Download Firefox
    - apk add firefox-esr
    - export FIREFOX_BIN=/usr/bin/firefox
    - export FIREFOX_PATH=/usr/lib/firefox/
    # X Server
    - apk add xvfb
    - export DISPLAY=:99
    - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
  script:
    - npm run test -- --browsers=Headless_Chrome --no-watch
  cache:
    key:
      files:
        - package.json
    paths:
      - node_modules
    policy: pull

assemble:
  stage: assemble
  needs: ["test", "lint"]
  script:
    - npm run build
  artifacts:
    paths:
      - $CI_PROJECT_DIR/dist
  cache:
    key:
      files:
        - package.json
    paths:
      - node_modules
    policy: pull

.deploy:
  stage: deploy
  variables:
    NODE_ENV: production
  needs: ["assemble"]
  before_script:
    - echo "Deploying to $CI_ENVIRONMENT_NAME"

deploy-dev:
  extends: .deploy
  variables:
    NODE_ENV: development
  script:
    - echo "Deploying to $CI_ENVIRONMENT_NAME"
    - echo "Making actions to deploy to dev"
    - echo "Deployed to $CI_ENVIRONMENT_NAME"

deploy-staging:
  extends: .deploy
  rules:
    - when: manual
      allow_failure: true
  variables:
    NODE_ENV: staging
  script:
    - echo "Deploying to $CI_ENVIRONMENT_NAME"
    - echo "Making actions to deploy to staging"
    - echo "Deployed to $CI_ENVIRONMENT_NAME"

Enter fullscreen mode Exit fullscreen mode

Overall, this CI/CD configuration is a solid foundation for building, testing, and deploying a Node.js project. The use of caching and artifacts can help speed up the build process, and the separation of stages allows for greater control over the entire pipeline.

Top comments (2)

Collapse
 
danielsc profile image
Daniel Schreiber

For running unit tests, with the right container, the setup can be simplified to:

test:
  stage: quality
  image: zenika/alpine-chrome:with-node
  script:
    - npm ci
    - npm run test-ci
Enter fullscreen mode Exit fullscreen mode

Where "test-ci" is defined in the package.json as the following script:

"test-ci": "ng test --no-watch --no-progress --browsers=ChromeHeadless --code-coverage"
Enter fullscreen mode Exit fullscreen mode
Collapse
 
nathanalcantara profile image
Nathan Alcantara

If you cache node_modules you should guarantee that every job will install it if not present, if you don't want that install every job you should use artifacts instead.

"Cache is here to speed up your job but it may not exist, so don't rely on it." about.gitlab.com/blog/2022/09/12/a...