A pipeline for a CLI gem
Give the gem a command
A gem becomes a command when the gemspec names an executable. The file goes into exe/, starts with a shebang, and loads the library like any other Ruby program:
#!/usr/bin/env ruby
require "version"
puts "ci-demo #{CiDemo::VERSION}"
The gemspec gains two lines and one file in its list:
spec.files = ["lib/todo.rb", "lib/version.rb", "exe/ci-demo"]
spec.bindir = "exe"
spec.executables = ["ci-demo"]
bindir says where the executables live and executables says which ones the package owns. RubyGems writes a launcher for each into the bin directory, so ci-demo works after the install without a path.
Build and install it
gem build ci-demo.gemspec
Successfully built RubyGem
Name: ci-demo
Version: 0.1.0
File: ci-demo-0.1.0.gem
gem install ./ci-demo-0.1.0.gem
Successfully installed ci-demo-0.1.0
Parsing documentation for ci-demo-0.1.0
Installing ri documentation for ci-demo-0.1.0
Done installing documentation for ci-demo after 0 seconds
1 gem installed
ci-demo
ci-demo 0.1.0
The three commands are the release path in miniature: build the package, install what you built, run it. A pipeline that does the same three steps in that order catches a broken gemspec before it reaches RubyGems.
The pipeline file
One file, three jobs, and a release job that only runs on a tag:
name: CI
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
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
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [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 rake release:check
env:
TAG: ${{ github.ref_name }}
- run: gem build ci-demo.gemspec
- run: gem push *.gem
env:
GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
The if: line is the gate: on a branch the release job is skipped, on a tag it runs after the tests and the linter. The version check from lesson 64 fails the job before the push when the tag and the gemspec disagree.
What to watch for
Test the installed package, not only the code in the repository. A file
missing from spec.files runs fine in the project
folder and fails from the installed gem; the three commands above, run in
one job, catch it.
Keep the build and the push in different steps. A build that fails before
the push leaves no published version behind, and the step log says which
of the two failed.
Next: A pipeline for a web API
Article author: Arthur Isaev