Making a pipeline fast

Measure first

A pipeline that feels slow is not a measurement. Two commands give the numbers that matter: the install and the tests, each timed on the same machine in the same state.

The install, with the bundle already warm

time bundle install

real 0m0.576s user 0m0.463s sys 0m0.112s

The tests

time bundle exec rake test

real 0m0.959s user 0m0.817s sys 0m0.141s

Under a second each, on a machine with the bundle already installed. A cold install is the interesting number: it downloads every gem and it is the step that a cache removes.

Where the seconds go

In a Ruby pipeline the time usually sits in three places: the bundle install (network), the test suite (CPU) and the image build (both). A cache fixes the first, a matrix multiplies the second, and a rebuild fixes the third.

The wrong move is to skip the tests or the linter to save time. A pipeline that is fast because it checks less is not faster; it is a different pipeline, and the first broken release explains the difference.

Parallel jobs and split tests

Jobs that do not depend on each other can run at the same time. In GitHub Actions that is two jobs; in GitLab CI two entries in the same stage:

jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" bundler-cache: true - run: bundle exec rake test lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: "3.4" bundler-cache: true - run: bundle exec rubocop

When one job is still the slow one, split it. A test file is a unit of work, so a job can take every third file:

files = Dir["test/*_test.rb"].sort mine = files.each_with_index.select { |_, index| index % 3 == 0 }.map(&:first) abort "nothing to run" if mine.empty? exec("ruby", "-Itest", *mine)

This project has one test file, so splitting it would not help; the pattern matters for a suite that takes minutes. Split by file and keep the split deterministic, or the same test lands in two jobs and in none.

What to watch for

Cache keys decide how often the cache helps. Key on the lock file, not on the branch: a feature branch that changes no gem should reuse the same bundle as the main branch.

Measure before and after each change, on the same machine. A new job that saves four seconds but doubles the queue time is a net loss, and only the numbers show it.

Next: Security scans

Article author: Arthur Isaev