Build and release a gem

Version and gemspec

A gem needs a name, a version and a list of files. Keep the version in one place, a small file the gemspec reads, so the tag, the changelog and the package always agree.

lib/version.rb

module CiDemo VERSION = "0.1.0" end

ci-demo.gemspec

require_relative "lib/version" Gem::Specification.new do |spec| spec.name = "ci-demo" spec.version = CiDemo::VERSION spec.summary = "A tiny todo library used by the CI lessons" spec.authors = ["Arthur Isaev"] spec.license = "MIT" spec.homepage = "https://www.aredel.com/dev/ruby/course/" spec.files = ["lib/todo.rb", "lib/version.rb"] spec.require_paths = ["lib"] spec.required_ruby_version = ">= 3.0" end

gem build

Build the package from the gemspec:

gem build ci-demo.gemspec

Successfully built RubyGem
Name: ci-demo
Version: 0.1.0
File: ci-demo-0.1.0.gem

The file lands next to the gemspec. Its name carries the version, so a new release is a new file and never an overwrite.

Install the package

Install the local file before pushing anywhere. A path with a slash or a dot is installed as a local package, which is the quickest end-to-end check:

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

If the install succeeds but require "todo" fails, the files list in the gemspec is incomplete: the package was built without that file. This is the most common gemspec mistake, and the local install is where it shows up.

gem push

Publishing sends the package to RubyGems, where Bundler can find it:

gem push ci-demo-0.1.0.gem

Publishing needs a rubygems.org account and an API key; the command reads the key from ~/.gem/credentials or from a secret that the pipeline passes in. The example gem of this course was not pushed, so there is no output to show for it: the command is here so you know where the release ends.

Keep the credentials file out of the repository and pass the key as a secret to the job.

In a pipeline, push only from a tag: build the package, check that the version in the gemspec matches the tag name, and then push. A release from a branch is a release nobody can reproduce.

What to watch for

The version lives in one file, and the git tag repeats it. A mismatch between the tag and the gemspec is easy to create by hand and hard to notice later, so let the pipeline read CiDemo::VERSION and compare it with the tag before the push.

Build the package in CI rather than on a laptop. The `.gem` file is reproducible from the repository only if nothing local leaks into the file list, and a build on a fresh machine is the easiest way to prove it.

Next: Auditing dependencies

Article author: Arthur Isaev