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