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