GitLab CI/CD stages are the fundamental building blocks of your pipeline, and understanding how they orchestrate jobs is key to building robust and efficient workflows.

Imagine this: you have a GitLab CI/CD pipeline that needs to build, test, and deploy your application. You’ve defined a .gitlab-ci.yml file with several jobs. But how does GitLab know which jobs to run, and in what order? That’s where stages come in.

Stages define the high-level phases of your pipeline. Jobs within the same stage run in parallel (if they can), and all jobs in a stage must complete successfully before any job in the next stage can begin. This sequential execution of stages is the core of how pipelines are structured and controlled.

Let’s look at a simple .gitlab-ci.yml that demonstrates this:

stages:
  - build
  - test
  - deploy

build_app:
  stage: build
  script:
    - echo "Building the application..."
    - make build

test_unit:
  stage: test
  script:
    - echo "Running unit tests..."
    - make test-unit

test_integration:
  stage: test
  script:
    - echo "Running integration tests..."
    - make test-integration

deploy_staging:
  stage: deploy
  script:
    - echo "Deploying to staging environment..."
    - make deploy-staging
  only:
    - main

In this example, we’ve defined three stages: build, test, and deploy.

The build_app job belongs to the build stage. The test_unit and test_integration jobs both belong to the test stage. The deploy_staging job belongs to the deploy stage.

When this pipeline runs, GitLab will first execute all jobs in the build stage. In this case, only build_app. Once build_app completes successfully, GitLab will move to the test stage. Here, test_unit and test_integration will run. Since they are in the same stage, they will run in parallel if GitLab has enough runners available. If either test_unit or test_integration fails, the pipeline will stop, and the deploy stage will not be reached. If both tests pass, then the deploy_staging job will execute.

The most surprising true thing about GitLab CI/CD stages is that the order of stages in the stages: keyword absolutely dictates the execution order, regardless of where you define the jobs in your .gitlab-ci.yml file. You could define the deploy stage jobs before the build stage jobs in the file, and GitLab would still execute build first, then test, then deploy. This separation of definition and execution order is crucial for maintaining clarity and flexibility in complex pipelines.

The exact levers you control are the stage names, the jobs assigned to each stage, and the order of the stage names in the stages: array. You can have as many stages as you need, and jobs can be assigned to any stage. You can also have stages with no jobs, or jobs that don’t belong to any stage (though this is less common and generally not recommended for structured pipelines).

Consider a scenario where you want to run code quality checks before your unit tests. You could simply add a new stage:

stages:
  - quality
  - build
  - test
  - deploy

lint_code:
  stage: quality
  script:
    - echo "Linting code..."
    - make lint

build_app:
  stage: build
  script:
    - echo "Building the application..."
    - make build

test_unit:
  stage: test
  script:
    - echo "Running unit tests..."
    - make test-unit

deploy_staging:
  stage: deploy
  script:
    - echo "Deploying to staging environment..."
    - make deploy-staging
  only:
    - main

Now, lint_code will run first. If it fails, the pipeline stops before any build or test jobs even start. This early feedback loop is invaluable for catching issues quickly.

The stages: keyword isn’t just for ordering; it also defines the available environments for your jobs. When a job runs, it’s assigned to a specific stage, and GitLab’s UI will visually represent this progression. This makes it easy to see where a pipeline is in its execution and where it might have failed.

You can also control which jobs run within a stage using rules or only/except keywords, which can be combined with stage definitions. For instance, you might have multiple testing stages, but only run certain tests on specific branches or tags.

The most common pitfall for beginners is assuming the order of jobs in the .gitlab-ci.yml file dictates execution order. This is false. Always rely on the stages: array for controlling the overall pipeline flow.

The next concept you’ll likely encounter is how to manage dependencies between jobs, even within the same stage, using needs.

Want structured learning?

Take the full Gitlab course →