Post

Ruby Build with Bundler and Rake

How Bundler and Rake build Ruby and Rails applications and gems, which files they need, how to lint, scan and test, and a complete GitHub Actions pipeline.

Ruby Build with Bundler and Rake

Ruby builds revolve around two tools: Bundler resolves, locks and installs gems from the Gemfile, and Rake is the task runner that drives building, testing and packaging. For Rails applications the “build” also includes precompiling assets; for libraries it means building and publishing a .gem.

This post is part of Build Ecosystems by Programming Language.

At a glance

CategoryCommon tools
Build toolsBundler, Rake, gem build; version managers: rbenv, rvm, asdf / mise
Package registryRubyGems.org, private gem servers (Artifactory, Gemfury, GitHub Packages)
Build filesGemfile, Gemfile.lock, *.gemspec, Rakefile, .ruby-version, .bundle/config, config.ru, .rubocop.yml
Lint and formatRuboCop (+ rubocop-rails, rubocop-rspec), Standard, Reek
SAST (open source)Brakeman (Rails), Semgrep, CodeQL, SonarQube Community Build
SAST (commercial)Checkmarx One, Veracode, Snyk Code, SonarQube Server
SCAbundler-audit, OSV-Scanner, Trivy, Snyk Open Source, Mend
FrameworksRuby on Rails, Sinatra, Hanami
Test and coverageRSpec, Minitest, Capybara; SimpleCov
Build output.gem package, Rails app with precompiled assets, container image
CI cachevendor/bundle (handled by ruby/setup-ruby with bundler-cache: true)

How the build works

In CI, each stage of the generic build pipeline maps to a Bundler / Rake command, plugin or scanner. Press play, or step through it:

  1. Source + Gemfile.lock: A commit triggers CI. Gemfile and Gemfile.lock pin every gem, and .ruby-version pins Ruby.
  2. Install: With bundle config set deployment true, bundle install installs exactly Gemfile.lock and fails if it would change.
  3. Lint and format: bundle exec rubocop checks style and common bugs (with rubocop-rails and rubocop-rspec); Standard is a zero-config alternative.
  4. Build assets: For Rails apps, bin/rails assets:precompile builds the production assets; plain gems skip this stage.
  5. Test and coverage: bundle exec rspec runs the specs, with SimpleCov loaded in spec_helper to measure coverage.
  6. Security scans in parallel: Brakeman checks Rails code for vulnerabilities, bundle-audit check --update checks Gemfile.lock against known advisories, and Gitleaks scans for secrets.
  7. Quality gate: RuboCop, the specs and both scanners are required checks; one failure stops the pipeline.
  8. Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
  9. Build the gem (pass): bundle exec rake build (Bundler's gem tasks) writes pkg/<name>-<version>.gem.
  10. SBOM and signing (pass): Syft generates the SBOM, and Sigstore cosign signs the gem.
  11. Publish (pass): gem push uploads the .gem to RubyGems.org or a private gem server.
  • bundle exec runs a command with exactly the gem versions in Gemfile.lock; always use it (or binstubs in bin/) in CI.
  • Platforms matter: Gemfile.lock lists the platforms it was resolved for. Add the Linux platform (bundle lock --add-platform x86_64-linux) when developers use macOS but CI and production run Linux.
  • Deployment mode (bundle config set deployment true) refuses to change the lockfile and installs into vendor/bundle.
  • New Rails applications ship with Brakeman, RuboCop and a GitHub Actions workflow already set up (bin/brakeman, bin/rubocop).

Build files

FilePurposeCommit?
GemfileGem sources, dependencies and groupsYes
Gemfile.lockExact gem versions, platforms and checksumsYes for apps; optional for gems
*.gemspecGem metadata and runtime dependencies (libraries)Yes
RakefileTask definitions (build, test, release)Yes
.ruby-versionRuby version for rbenv / setup-rubyYes
.bundle/configLocal Bundler settingsNo (CI sets them)
config.ruRack entry point for web appsYes
.rubocop.ymlLint configurationYes
vendor/bundle, pkg/Installed gems, built gemsNo

Commands by category

1
2
3
4
5
6
7
8
bundle config set deployment true && bundle install --jobs 4   # reproducible install
bundle exec rubocop                              # lint
bundle exec brakeman --no-pager                  # SAST (Rails)
bundle exec bundle-audit check --update          # SCA
bundle exec rspec                                # tests (SimpleCov loaded in spec_helper)
RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bin/rails assets:precompile   # Rails asset build
bundle exec rake build                           # build pkg/demo-1.0.0.gem (Bundler gem tasks)
gem push pkg/demo-1.0.0.gem                      # publish

Minimal project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Gemfile
source "https://rubygems.org"

ruby file: ".ruby-version"

gem "rails", "~> 8.0"
gem "puma"

group :development, :test do
  gem "rspec-rails"
  gem "rubocop-rails-omakase", require: false
  gem "brakeman", require: false
  gem "bundler-audit", require: false
  gem "simplecov", require: false
end
1
2
3
4
5
6
# Rakefile for a gem: adds build, install and release tasks
require "bundler/gem_tasks"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec)
task default: :spec

Frameworks

FrameworkStart a project
Ruby on Railsgem install rails && rails new app
Sinatrabundle add sinatra puma and a config.ru with run Sinatra::Application
Hanamigem install hanami && hanami new app
A gembundle gem demo (creates gemspec, Rakefile, tests)

CI with GitHub Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name: ruby-ci
on: [push, pull_request]
permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true                  # bundle install + cache vendor/bundle

      - run: bundle exec rubocop
      - run: bundle exec brakeman --no-pager
      - run: bundle exec bundle-audit check --update
      - run: bundle exec rspec
      - run: bundle exec rake build

      - uses: actions/upload-artifact@v4
        with:
          name: gem
          path: pkg/*.gem

Best practices

  • Always run through bundle exec (or binstubs) so the locked versions are used.
  • Commit Gemfile.lock with the Linux platform added, and install in deployment mode in CI.
  • Pin Ruby with .ruby-version and read it from the Gemfile (ruby file: ".ruby-version").
  • Run Brakeman and bundler-audit on every PR; both are fast and catch the most common Rails issues.
  • Use a private gem source for internal gems with a source "https://..." do block, so internal names cannot be hijacked from RubyGems.org.
  • Precompile assets in the image build, not at container start.
This post is licensed under CC BY 4.0 by the author.