What a CI pipeline is

What a pipeline is

A continuous integration pipeline runs the commands a developer runs by hand, but on a fresh machine and for every push. It checks out the repository, installs the gems, runs the tests and the linters, and reports one result: green when every command exited with zero, red when one did not.

The point is not the automation itself. It is the fresh machine: no leftover files, no gem installed months ago, no local edit that the repository does not have. A run that passes there is a claim another person can repeat.

This chapter builds one small project and adds one piece of the pipeline per lesson: the pipeline file (GitHub Actions or GitLab CI), the Rake task that runs everything, the gem cache, the linter, the test report, the image, the release and the dependency audit. Every command below was run in the Ruby image of this course, and the output is that run.

The example project

One class with three methods and three tests is enough to carry the whole chapter. Create the folder and these five files:

ci-demo ├── Gemfile ├── Rakefile ├── lib │ └── todo.rb └── test └── todo_test.rb

Gemfile

source "https://rubygems.org" gem "minitest", "~> 5.20" gem "rake", "~> 13.0"

lib/todo.rb

class Todo attr_reader :items def initialize @items = [] end def add(title) @items << { title: title, done: false } @items.last end def done(index) @items[index - 1][:done] = true end def list @items.each_with_index.map do |item, index| "#{index + 1}. #{item[:title]}#{item[:done] ? " (done)" : ""}" end end end

test/todo_test.rb

require "minitest/autorun" require_relative "../lib/todo" class TodoTest < Minitest::Test def setup @todo = Todo.new end def test_add_returns_the_item item = @todo.add("Buy milk") assert_equal "Buy milk", item[:title] refute item[:done] end def test_list_numbers_the_items @todo.add("Buy milk") @todo.add("Write tests") assert_equal ["1. Buy milk", "2. Write tests"], @todo.list end def test_done_marks_an_item @todo.add("Buy milk") @todo.done(1) assert_equal ["1. Buy milk (done)"], @todo.list end end

Rakefile

require "rake/testtask" Rake::TestTask.new(:test) do |task| task.libs << "test" task.pattern = "test/*_test.rb" end task default: :test

What a job runs

With the files in place, two commands reproduce what a pipeline does: install the gems from the lock file, then run the tests.

Install the gems

bundle install

Fetching gem metadata from https://rubygems.org/...
Resolving dependencies...
Bundle complete! 2 Gemfile dependencies, 2 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

Run the tests

bundle exec rake test

Run options: --seed 8918

# Running:

...

Finished in 0.001315s, 2281.3081 runs/s, 3041.7441 assertions/s.

3 runs, 4 assertions, 0 failures, 0 errors, 0 skips

Three dots, three tests, four assertions. This pair of commands is the whole job: everything else in the chapter wraps it in a file and adds a cache, a linter and a report. The seed and the timings change on every run; the last line is the result that the pipeline reads.

Stages

A pipeline is split into stages, and each stage into jobs. One common shape for a Ruby project:

pipeline ├── setup │ └── checkout + bundle install (cached) ├── test │ └── bundle exec rake test ├── lint │ └── cop check └── audit └── advisory check on Gemfile.lock

Stages run one after another; the jobs inside a stage may run in parallel. A cache carries files between runs (the installed gems), an artifact carries files from a run to the person reading the result (the test report, a built package). Both are optional, and both can go stale in ways that hide a bug, so the next four lessons treat them with care.

What to watch for

A green run means the commands exited with zero on that machine. It does not mean the code is correct, the tests are good, or the same run will pass tomorrow: a floating gem version or a changed base image can turn green into red overnight.

Two habits keep a pipeline honest: pin what you can (the lock file, the Ruby version, the image tag) and make the pipeline run exactly the command you run locally, so a failure on either side is reproducible.

Next: Ruby in GitHub Actions

Article author: Arthur Isaev