GitLab CI Basic Pipeline

A three-stage .gitlab-ci.yml pipeline: build a Node.js app, run tests with coverage reporting, and deploy to production only on the main branch.

🛠 Paste this YAML into the validator to check it instantly.

Open Validator →

Overview

GitLab CI/CD is driven entirely by a .gitlab-ci.yml file placed at the root of your repository. When you push a commit, GitLab reads this file and creates a pipeline: an ordered series of stages, each containing one or more jobs that run in parallel. This example shows the most fundamental pattern — build, test, deploy — which serves as the backbone for the vast majority of real-world Node.js projects on GitLab.

Unlike some CI systems where the configuration can sprawl across multiple files and UI settings, GitLab CI keeps everything in one place. The result is a pipeline that is easy to review in code review, easy to reproduce locally via gitlab-runner exec, and easy to evolve as your project grows.

Complete YAML

.gitlab-ci.yml
stages:
  - build
  - test
  - deploy

variables:
  NODE_VERSION: "20"

build:
  stage: build
  image: node:${NODE_VERSION}
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 day

test:
  stage: test
  image: node:${NODE_VERSION}
  script:
    - npm ci
    - npm test
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'

deploy:
  stage: deploy
  script:
    - echo "Deploying to production"
    - ./scripts/deploy.sh
  environment:
    name: production
    url: https://example.com
  only:
    - main

Stages and jobs

The top-level stages key defines the execution order of your pipeline. Stages run one after the other — if any job in a stage fails, GitLab stops the pipeline and does not proceed to the next stage. Within a single stage, all jobs run in parallel (assuming you have enough runners available), which keeps total pipeline time short.

Each job is assigned to a stage via its stage key. The image key per job tells the GitLab runner which Docker image to spin up for that job's execution environment. Here, both build and test use node:${NODE_VERSION}, where the version is centralised in the top-level variables block — change it in one place and every job picks it up automatically. You can override image on a per-job basis, which is useful when your deploy job needs a cloud CLI tool image rather than Node.

The script key is an ordered list of shell commands that the runner executes inside the container. Commands run in a single shell session, so environment variables set in one command are available to the next. If any command exits with a non-zero status, the job is marked as failed.

The coverage key in the test job accepts a regular expression that GitLab applies to the job's log output to extract a coverage percentage. The regex shown here is compatible with Istanbul / nyc / Jest's default table format. Once extracted, GitLab displays the coverage badge on the repository, shows the value in merge request pipelines, and can enforce a minimum coverage threshold via project settings.

Artifacts

The artifacts block on the build job tells GitLab to preserve the dist/ directory after the job finishes and make it available to downstream jobs in later stages. Without this, each job starts with a clean checkout of the repository and would have to re-run npm run build itself. Artifacts are uploaded to the GitLab server (or your object storage backend) and downloaded by subsequent jobs automatically.

The expire_in: 1 day setting controls how long GitLab stores the artifact. For intermediate build outputs that only need to pass between stages in a single pipeline run, a short expiry keeps your storage costs low. You can increase this — or remove the limit entirely — for artifacts you want to download manually from the GitLab UI for debugging or release distribution.

By default, artifacts from all previous stages are automatically downloaded at the start of each subsequent job. If you want to skip this for a specific job to save bandwidth, add dependencies: [] to that job's definition.

Deploy restrictions and environments

The only: [main] key restricts the deploy job so it only runs when the pipeline was triggered on the main branch. Push to any feature branch and GitLab will run build and test, but skip deploy entirely. This is the classic trunk-based deployment pattern: merge to main to ship.

The modern equivalent of only is rules, which offers a richer expression language using if, changes, and exists conditions. For simple branch filtering, only remains readable and perfectly functional; for more complex logic (e.g. "run only on merge requests targeting main, but skip if the commit message contains [skip ci]"), migrate to rules.

The environment keyword registers this job with GitLab's Environments feature. GitLab tracks deployments per environment, shows a timeline of who deployed what and when, and enables features like environment-scoped CI/CD variables (so your production database URL is only visible to jobs deploying to production) and one-click rollback from the Deployments UI. The url field adds a "Visit" button in the GitLab pipeline view that links directly to the live application.

Tip: Combine environment with when: manual to require a human to click "Play" in the GitLab UI before the deploy job executes — useful when you want automatic staging deploys but gated production releases.