RSS

Nx + Gitlab CI/CD

Working with Gitlab and Nx might not be as straightforward as the official guide. It assumes you can work with merge_request_event and your current flow won’t be disturbed. On top of that it can lead to duplicated pipelines and make your pipelines tricky to optimize.

Additionally, I have also noticed that Gitlab won’t show status from multiple pipelines in the UI! This is pretty bad UX that can lead to introducing regression to your main branch or even your production system.

What’s wrong with the official setup?

Example: You just opened a PR and pushed a new commit to your branch. In jobs definition you have two events:

  • “push” - run job1
  • “merge_request_event - run job2 (eg. nx affected command)

If one of the jobs fails, this is what you might end up seeing:

Gitlab Status Widget

Failed status is nowhere to be find. The only way to see it is to open “Pipelines” tab and look for it there.

Adjusting to “push” pipelines

Working with “push” was a constrain coming from Centralized CI/CD at the company. It standardizes the way of running CI jobs across dozens of projects. It makes infra consistent and easier to debug / adjust. Adding new job type is as easy as extending only one repo.

Main difference between “merge_request_event” and “push” is that some environment variables are not available. Predefined variables like CI_MERGE_REQUEST_DIFF_BASE_SHA are simply not available and you need a workaround in order to get what you want. Luckily you can always fallback to native git command and easily get it.

Here is a code snippet how I achieved it.

# .gitlab-ci.yaml

stages:
  # ...
  - pre-tests
  - tests
  # ...

npm-install:
  stage: pre-tests
  cache: 
    key: ${CI_PROJECT_PATH_SLUG}-${CI_COMMIT_BRANCH}
    paths:
      - node_modules/
    policy: push
  rules:
    - if: $CI_PIPELINE_SOURCE == "push"
  script:
    - node -v
    - npm -v
    - npm ci

extract-sha:
  stage: pre-tests
  image: alpine:3.18.2
  rules:
    - if: $CI_PIPELINE_SOURCE == "push"
  before_script:
    - apk add --update git
    - git fetch origin $CI_DEFAULT_BRANCH
  script:
    - BASE_SHA=$([ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ] && echo "$CI_COMMIT_BEFORE_SHA" || git merge-base HEAD origin/$CI_DEFAULT_BRANCH)
    - echo 'BASE_SHA' ${BASE_SHA}
    - echo "export BASE_SHA=$BASE_SHA" > $CI_PROJECT_DIR/sha.txt
  artifacts:
    when: always
    paths:
      - sha.txt
    expire_in: 1 day

.npm-task:
  stage: tests
  needs:
    - job: extract-sha
    - job: npm-install
      optional: true
  rules:
    - if: $CI_PIPELINE_SOURCE == "push"
  before_script:
    # nx affected needs git, which alpine doesn't have. So it's added here.
    - apk add --update git
    - source $CI_PROJECT_DIR/sha.txt
    - NX_BASE=$BASE_SHA
    - NX_HEAD=$CI_COMMIT_SHA
    - echo 'NX_BASE' ${NX_BASE}
    - echo 'NX_HEAD' ${NX_HEAD}

affected/lint:
  extends: .npm-task
  script: npx nx affected --target=lint --base=$NX_BASE --head=$NX_HEAD

affected/test:
  extends: .npm-task
  script: npx nx affected --target=test --base=$NX_BASE --head=$NX_HEAD