Dark navy and teal Ruby for DevOps graphic reading Test It Ship It

Ruby for DevOps — Part 5 of 5

Automation can change infrastructure quickly, which is exactly why it deserves tests. A short script can stop a deployment, delete the wrong resource, or quietly break a pipeline contract. Treating Ruby DevOps code as software—not disposable glue—gives a team safer changes and more freedom to extend it.

This final tutorial makes the project repeatable with Bundler, Rake, Minitest, syntax checks, safe smoke commands, and GitHub Actions. GitHub’s official Ruby Actions guide uses ruby/setup-ruby, Bundler, bundle exec rake, matrix testing, and Bundler caching.1

Fork or follow the code

The source is in the DevOps Coach repository. Open the exact Gemfile, Rakefile, test directory, and copy-ready Ruby CI template.

gh repo fork jjam3774/devop-coach --clone
cd devop-coach/tutorials/ruby-for-devops/code
bundle install

Or follow upstream:

git clone https://github.com/jjam3774/devop-coach.git
cd devop-coach/tutorials/ruby-for-devops/code

A fork is the better route for the exercises because you can push a branch, observe CI, and open a pull request.

Lock the development environment with Bundler

The production code uses Ruby’s standard library. The Gemfile adds Minitest and Rake for development:

source "https://rubygems.org"

ruby ">= 3.2", "< 4.0"

gem "minitest", "~> 5.25"
gem "rake", "~> 13.2"
bundle install
bundle exec rake

Commit both Gemfile and Gemfile.lock. Bundler documents that the lockfile gives collaborators and deployment environments the same third-party code.2 bundle exec ensures the selected Rake and Minitest versions come from the project bundle instead of an unrelated system installation.

Use one Rake task as the validation contract

Task Responsibility External side effects
syntax Run ruby -c across executables, examples, libraries, and tests. None
test Run every Minitest file. None; network and child-process tests are controlled.
smoke Exercise safe CLI paths and deterministic examples. No infrastructure changes; command execution uses dry-run.
desc "Run all validation tasks"
task default: %i[syntax test smoke]

This creates a single contract:

bundle exec rake

If that command passes locally and in CI, the repository has checked syntax, behavior, CLI wiring, sample configuration, and example output.

Check syntax before behavior

task :syntax do
  ruby_files = FileList[
    "bin/*",
    "examples/**/*.rb",
    "lib/**/*.rb",
    "test/**/*.rb"
  ]

  ruby_files.each { |file| sh RbConfig.ruby, "-c", file }
end

This catches parse errors quickly. It does not replace tests: a syntactically valid script can still be unsafe or wrong.

bundle exec rake syntax

Write deterministic unit tests

Minitest ships a compact Ruby testing style and supports assertions and standard test patterns.3 A command-runner test uses the current Ruby executable as a deterministic child process:

result = @runner.run([
  RbConfig.ruby,
  "-e",
  "STDOUT.write('ready'); STDERR.write('note')"
])

assert result.success?
assert_equal "ready", result.stdout
assert_equal "note", result.stderr
assert_equal 0, result.exit_status

The timeout test is local too:

result = @runner.run(
  [RbConfig.ruby, "-e", "sleep 2"],
  timeout: 0.05
)

refute result.success?
assert result.timed_out
assert_nil result.exit_status

Configuration tests create temporary YAML files. Health-check tests inject a fake transport. CLI tests use StringIO. No unit test requires cloud credentials or a public endpoint.

bundle exec ruby -Itest test/command_runner_test.rb
bundle exec rake test

Test safety properties, not only happy paths

Component Important properties under test
Command runner stdout and stderr capture, nonzero status preservation, missing executable, timeout, dry run, metacharacters as data, empty-command rejection
Configuration loader valid defaults, non-empty services, HTTP/HTTPS restriction, status range, disabled YAML aliases, missing files
Health checker expected status, required failure, transport exception, optional failure, bounded concurrency, stable output order
CLI JSON output, dry run, configuration exit code, check exit code, unknown-command usage failure

The shell-metacharacter test passes ; echo this-must-not-run as one argument and verifies that the child receives exactly that string. This guards the design decision to avoid an implicit shell.

Add safe smoke commands

task :smoke do
  sh RbConfig.ruby, "-Ilib", "bin/devops-toolkit", "doctor", "--json"
  sh RbConfig.ruby, "-Ilib", "bin/devops-toolkit",
     "config", "--file", "config/services.yml", "--json"
  sh RbConfig.ruby, "-Ilib", "bin/devops-toolkit",
     "run", "--dry-run", "--json", "--", "printf", "hello-from-ruby"
  sh RbConfig.ruby, "examples/quick_compare.rb"
  sh RbConfig.ruby, "examples/log_report.rb", "config/events.jsonl"
end

The smoke task deliberately does not call terraform apply, kubectl apply, or live health endpoints. Put credentialed integration tests in a separately protected workflow with explicit permissions and environment controls.

Run the same contract in GitHub Actions

The repository ships the workflow as a copy-ready template. Activate it in your fork:

mkdir -p .github/workflows
cp tutorials/ruby-for-devops/ci/ruby-devops.yml \
  .github/workflows/ruby-devops.yml

The activated workflow grants only read-only source permission and tests a Ruby version matrix:

name: Ruby DevOps Tutorials

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        ruby: ["3.2", "3.3", "3.4"]
- name: Set up Ruby ${{ matrix.ruby }}
  uses: ruby/setup-ruby@v1
  with:
    ruby-version: ${{ matrix.ruby }}
    bundler-cache: true
    working-directory: tutorials/ruby-for-devops/code

- name: Validate the tutorial toolkit
  working-directory: tutorials/ruby-for-devops/code
  run: bundle exec rake

Matrix testing proves the declared Ruby range works in more than one interpreter version. fail-fast: false lets every matrix job finish, giving a full compatibility picture.

Push a branch and observe CI

git checkout -b ruby-devops-extension
bundle exec rake
git add tutorials/ruby-for-devops .github/workflows/ruby-devops.yml
git commit -m "Extend the Ruby DevOps toolkit"
git push -u origin ruby-devops-extension

Open a pull request on GitHub. The Ruby DevOps Tutorials workflow should run once per Ruby version. If a job fails, reproduce bundle exec rake locally from the code directory. Do not remove a version merely to make CI green unless the project deliberately changes its support policy.

Keep CI permissions boring

A test workflow for this project only needs source access. It does not need write permission, a cloud token, or a deployment environment. If you later add deployment, create a separate workflow or job with environment protection, explicit approvals, pinned actions, and narrowly scoped credentials.

How this creates flexibility

Tests protect the interface while implementation changes. You can replace Net::HTTP, add a JSON inventory source, package the toolkit as a gem, or call it from Python; if result formats and exit codes remain covered, callers stay stable. A version matrix provides runtime flexibility. A locked bundle provides dependency consistency. Injected transports and streams provide test flexibility. These choices make a Ruby component a better citizen in a mixed DevOps system, not a requirement to choose Ruby everywhere.

Series complete

You now have a Ruby DevOps option that is more than a syntax demonstration. The project exposes a stable CLI, executes commands without an implicit shell, validates YAML into a narrow schema, checks services concurrently, streams JSON log data, and validates itself locally and in CI.

Return to the Ruby for DevOps series landing page for the full path and Python-to-Ruby capability map.

References

  1. GitHub Docs: Building and Testing Ruby
  2. RubyGems Guides: Getting Started with Bundler
  3. Ruby Standard Library