Building a Rock-Solid Angular CI/CD Pipeline with GitLab: A Step-by-Step Guide
Tags: angular, webdev, gitlab, typescript
Published: 2023-04-26 03:55:24

[GitLab CI/CD](https://docs.gitlab.com/ee/ci/) 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:

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

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:

```yaml
stages:
  - dependencies
  - quality
  - assemble
  - deploy
```
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:

```yaml
install:
  stage: dependencies
  script:
    # Install dependencies
    - npm install --prefer-offline
  cache:
    key:
      files:
        - package.json
    paths:
      - node_modules
```
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:
```yaml
lint:
  stage: quality
  needs: ["install"]
  script:
    - npm run lint
  cache:
    key:
      files:
        - package.json
    paths:
      - node_modules
    policy: pull
```
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:

```yaml
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
```
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
```javascript
// 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"],
      },
    }
  });
};
```

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

```yaml
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"

```


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.