Stageless Pipelines in GitLab

The release of GitLab 14.2 brings an exciting new feature to the management of CI/CD pipelines. Stages can now be completely omitted. Instead, the dependencies between pipeline jobs can be specified using the needs keyword. In this article, I will go into these changes and show you an example pipeline definition file making use of stageless pipelines.

Traditionally, CI/CD pipelines in GitLab would consist of multiple stages and each stage would contain multiple jobs:

The reasoning behind this structure was that all jobs of a single stage would be executed before jobs of the next stage, while all jobs of the same stage could be run in parallel. This execution model is fine for simple pipelines, but it restricts the performance of more complex ones.

Instead, a better solution is to specify for each job individually on which other jobs it depends on. This is why the needs keyword was introduced to GitLab. However, until GitLab 14.2, it was still required to define stages. Gladly, this has changed. Let’s have a look at this new way of writing pipelines.

Simple Pipeline Examples

We will start with a basic pipeline template as provided by gitlab.com:

stages:
  - build
  - test
  - deploy

build-job:
  stage: build
  script:
    - echo "Compiling the code..."
    - echo "Compile complete."

unit-test-job:
  stage: test
  script:
    - echo "Running unit tests... This will take about 60 seconds."
    - sleep 60
    - echo "Code coverage is 90%"

lint-test-job:
  stage: test
  script:
    - echo "Linting code... This will take about 10 seconds."
    - sleep 10
    - echo "No lint issues found."

deploy-job:
  stage: deploy
  script:
    - echo "Deploying application..."
    - echo "Application successfully deployed."

This pipeline defines three stages (build, test and deploy) which consist of one, two, and one job, respectively. The only stage with more than one job is the test stage that contains both the unit-test-job as well as the lint-test-job.

We can transform this simple pipeline into a stageless one in two steps:

  • Remove the stages: definition block
  • Replace each stage: keyword with a needs: keyword that references all the job dependencies

The result of this transformation is the following configuration:

build-job:
  script:
    - echo "Compiling the code..."
    - echo "Compile complete."

unit-test-job:
  needs: [build-job]
  script:
    - echo "Running unit tests... This will take about 60 seconds."
    - sleep 60
    - echo "Code coverage is 90%"

lint-test-job:
  needs: [build-job]
  script:
    - echo "Linting code... This will take about 10 seconds."
    - sleep 10
    - echo "No lint issues found."

deploy-job:
  needs: [unit-test-job, lint-test-job]
  script:
    - echo "Deploying application..."
    - echo "Application successfully deployed."

As you can see, this new configuration is both shorter (30 lines before, 24 lines after) and more explicit (for each job, you can see the dependencies directly in the job’s block).

When you execute this pipeline, you can also visualize the dependencies in GitLab’s Pipeline view:

With this new configuration, we have actually just replicated the same pipeline as before just without stages – but where is the advantage? To demonstrate why this flexibility can result in better overall performance, consider adding a new job that only depends on the result of the lint-test-job. If we still had stages, we would need to add this new job in a new stage between the test and deploy stage. However, then the new job would also need to wait for the unit-test-job to complete, even though the new job doesn’t care at all about those results.

In the new stageless world, this change is as simple as adding this configuration block:

post-linting-job:
  needs: [lint-test-job]
  script:
    - echo "Postprocessing linting results... This will take about 10 seconds."
    - sleep 10
    - echo "Postprocessing completed."

The resulting pipeline automatically schedules the new job after the lint-test-job is completed:

And here is the most beautiful thing: the new post-linting-job is already executed while the unit-test-job is still running, thereby improving the overall execution time of the pipeline.

Complete template

I encourage you to experiment with this new feature as it will improve both the clarity and the performance of your CI/CD pipelines. Feel free to start based on my template:

build-job:
  script:
    - echo "Compiling the code..."
    - echo "Compile complete."

unit-test-job:
  needs: [build-job]
  script:
    - echo "Running unit tests... This will take about 60 seconds."
    - sleep 60
    - echo "Code coverage is 90%"

lint-test-job:
  needs: [build-job]
  script:
    - echo "Linting code... This will take about 10 seconds."
    - sleep 10
    - echo "No lint issues found."

post-linting-job:
  needs: [lint-test-job]
  script:
    - echo "Postprocessing linting results... This will take about 10 seconds."
    - sleep 10
    - echo "Postprocessing completed."

deploy-job:
  needs: [unit-test-job, lint-test-job]
  script:
    - echo "Deploying application..."
    - echo "Application successfully deployed."