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

Banner image