whoami

Lucas Jenß

cat /etc/motd

The Coding Journal ツ — Notes taken on an epic coding journey. Technical solutions, debugging notes, and practical guides from the trenches of software development.

Close-up of white dominoes with black dots standing on green felt surface, shallow depth of field, focused mood

ls -la ~/languages/

total 8
drwxr-xr-x
▶ PHP
▶ Ruby
▶ Scala
▶ C#
▶ JavaScript
▶ Objective-C
▶ Shell Scripting

ls -la ~/toolchain/

total 7
drwxr-xr-x
▶ Typo3
▶ Akka
▶ Capistrano
▶ Git
▶ MAMP
▶ Adobe Illustrator
▶ NSTrackingArea (Cocoa)

uname -a

platforms
drwxr-xr-x
▶ Mac OS X
▶ Unix

Building a Ruby CLI tool with Thor that integrates with Capistrano

Most deployment scripts start small — a few Capistrano tasks here, a Sidekiq restart there — and then quietly grow until nobody is sure what they actually do. Packaging the most common pieces into a proper command-line tool gives the team a single entry point, proper help output, and a place to hang documentation. Thor handles the structural work nicely, and the hooks into Capistrano are straightforward once you understand the boundaries between the two.

I've been writing and maintaining internal tooling from Melbourne, often for teams that run split rosters between Sydney, Brisbane, and Perth. The pattern that keeps working across those time zones is one focused CLI binary that everyone on call knows how to reach. AEST makes handoffs to the west coast mildly awkward, so a tool that prints a sensible summary after running three or four deployment steps saves more late-evening brain cycles than it should.

Getting started with Thor for command-line work

Thor is one of those Ruby libraries that quietly appears under almost every Rails project but rarely gets to run on its own. It powers generators and is what Bundler uses for bundle gem. The Thor class is the entry point: each public method on it becomes a subcommand, and desc plus method_option give you help text, flags, and argument metadata essentially for free.

A minimal starter looks like this:

class Deploy < Thor
  desc "stage SERVER", "deploy to a named stage"
  method_option :branch, default: "main"
  def stage(server)
    puts "Deploying branch #{options[:branch]} to #{server}"
  end
end

Deploy.start(ARGV)

Adding map renames the binary, and package_name / stop_on_unknown_option! make help output behave like a polished tool rather than a script. For teams who have let Rails generators dictate style for years, Thor feels familiar without the leftover convention magic.

Planning commands around real deployment workflows

Before writing a second method, draw the commands you actually need on a whiteboard — or on the wall of your Brisbane office if that is your style. Common ones in my own CLI: status, rollback, logs, doctor, ssh, and a generic run that forwards to a Capistrano task. Each one should answer a question an on-call engineer in Adelaide might ask at 23:30 without thinking too hard.

Group related commands with desc namespaces and a subcommand class:

desc "logs SUBCOMMAND", "tail logs from a deployed role"
subcommand "logs", Logs

Logs is its own Thor class, which keeps the file tree sane as the CLI grows. Avoid the temptation to model every Capistrano task one-to-one: a deployment tool that runs 40 micro-commands is harder to memorise than the original Rakefile it replaced. Pick the high-frequency actions, expose them with clean flags, and let run catch the long tail.

Wiring the Thor CLI into Capistrano tasks

Once the commands feel right, calling into Capistrano is mostly about respecting its public interface. From outside a Rakefile, the cleanest path is to invoke Capistrano::Application directly and pass an array of arguments:

require "capistrano"

def run_cap(*args, env: {})
  logger = Capistrano::Logger.new($stdout, $stderr, level: :info)
  app = Capistrano::Application.new
  app.invoke(args, **env.transform_keys { |k| "env.#{k}" })
end

Inside the calling command, something like:

desc "deploy STAGE", "deploy the current branch to a stage"
def deploy(stage)
  run_cap("deploy", "deploy:updated", "deploy:restart", env: { stage: stage })
end

The trick is that Capistrano expects its configuration to have loaded already. If your project uses the capistrano/setup require, which most modern setups do, load that file before invoking anything. For older projects still on the deploy.rb-driven style, you may need to eval the file first, which feels brittle — but it works.

A useful pattern is to set SSHKit.config.backend to Print when the user passes --dry-run, so the CLI doubles as a deployment preview tool for code review. Senior engineers in Sydney often want to see exactly what will run before approving a change, and that one-line swap makes the difference between a tool people trust and one they bypass.

Managing configuration, secrets, and environment variables

CLI tools need a configuration source that does not live inside the project. Most teams I've worked with in Melbourne store per-engineer and per-team overrides in ~/.config/<tool>/config.yml, with fallbacks to environment variables. Thor doesn't ship a config loader, but dry-configurable or a thin wrapper around YAML.load_file with ERB is enough.

def config_path
  File.join(Dir.home, ".config", "deploy-tool", "config.yml")
end

def config
  @config ||= begin
    file = ERB.new(File.read(config_path)).result
    YAML.safe_load(file, permitted_classes: [Symbol])
  rescue Errno::ENOENT
    {}
  end
end

The most common footgun is hard-coding AWS, Linode, or DigitalOcean credentials inside the tool. Treat every secret as an ENV.fetch with a clear error message — fail loudly, never silently default to production. A surprising amount of Australian fintech and e-commerce work still uses Sydney-region AWS, so make sure region defaults do not silently cross borders into Singapore or Tokyo when a credential rotates.

For commands that fan out across multiple SSH hosts, hook into ~/.ssh/config instead of maintaining another inventory file. SSHKit::Backend::Netssh already reads it for you; engineers in Perth and Adelaide especially appreciate not having to maintain a separate inventory.yml just to connect to boxes.

Testing, packaging, and shipping the tool

A CLI that ships without tests regresses within a month. Three layers are worth the effort: unit tests for individual Thor commands using capture_io, integration tests against a real Capistrano repo (even a tiny one in spec/fixtures), and a smoke test that installs the gem fresh and runs --help. RSpec or Minitest both work; pick the one your team already uses.

Packaging is where most internal tools quietly die. A single Gem::Specification block, a bin/deploy shebang, and a bundler/gem_tasks import keeps the lifecycle boring in the best way. Push the gem to a private server — most Melbourne-based shops use Artifactory or a self-hosted Gemstash — so installs stay reproducible across the org.

Once installed, document the workflow in a single place. A short usage page, ideally alongside whatever shared documentation the team already keeps, beats a long README that nobody finds. When the CLI is genuinely useful, copy-paste happens within weeks, so design for that: friendly errors, predictable exit codes, and a --help output that reads like a cheat sheet for someone picking it up at 02:00 in a Sydney morning.

Ready to cut your own deployment friction? Spin up a fresh repo, drop a Thor class into lib/, and try replacing three of your most-used Capistrano tasks with subcommands by the end of the week. If you want to see how I structured my earlier experiments — including the awkward first attempts — the archive at the coding journal has them all, along with notes from a few non-deployment side projects like the trusted casino american roulette post I wrote while waiting on a long provision. And when your CLI starts to look more like an orchestrator than a command, the designing a resilient vsan cluster guide is a good place to borrow patterns for fault-tolerant coordination across nodes.


cat ~/interests.json

KeyValue
editorTerminal-first workflow
osMac OS X / Unix
vcsGit, distributed version control
deployCapistrano, cron automation
graphicsSVG, Adobe Illustrator troubleshooting
networkingIP validation, SSH, VPN

git log --oneline --reverse

2013-10-30

Solving SVG import issues in Adobe Illustrator CS6 and CC

When importing an SVG into Illustrator, the operation fails with an unknown error [CANT]. A workaround for this Adobe-side bug.

2013

Solving NDK build issues on OS X

Troubleshooting native development kit compilation problems on Mac OS X.

2013

Programmatically adding PHP generated TypoScript to the backend configuration

Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.

2013

ArgumentError: Could not parse PKey: no start line

Debugging an SSH key parsing error encountered during deployment.

2011-08-04

Validating IP-Addresses in PHP

Using PHP filter functions with flags like FILTER_FLAG_IPV4 and FILTER_FLAG_IPV6, and understanding how filter_var handles reserved IP addresses.

2011-07-09

Cocoa: Using NSTrackingArea

A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.


cat ~/contact.txt

github: github.com/x3ro
stackoverflow: x3ro
coderwall: coderwall.com/x3ro
twitter: @x3rames