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 DSL for Capistrano role configurations

Capistrano configuration is Ruby, yet a growing deployment file can become difficult to scan. Server names, role memberships and task definitions compete for attention, while a misplaced stage override quietly changes which machines receive a command. A purpose-built domain-specific language can make those relationships explicit without replacing Capistrano.

The aim is not to hide Ruby behind a grand abstraction. It is to give recurring deployment decisions a compact vocabulary: roles describe responsibility, servers describe membership, and stages describe environmental differences. Capistrano still owns releases, SSH connections and task execution; the new layer only normalises role declarations and their translation.

That boundary matters when a release fails during an Australian trading day. The useful question becomes “which servers belonged to this role in this stage?” rather than “which line changed the host list?” A small Ruby layer can preserve the answer as structured data, validate it early and print it during deployment checks.

It must also work across Australian operating realities. Teams in Sydney and Melbourne often use local cloud regions for latency and data residency, while Darwin and Brisbane run on UTC offsets without the same daylight-saving changes. Australian retailers and SaaS providers need predictable releases for .com.au services and busy shopping periods.

Separating roles, servers and stages

Begin by separating deployment vocabulary from implementation details. A role such as :app or :worker should identify a capability without implying that every host runs the application identically. Servers attach to those capabilities, while optional settings supply the SSH user, port or other connection details.

Keep stages out of the core role model. A stage selects and overrides values, but it should not silently create a second definition of the same role. This prevents a production-only host from becoming a global default and makes the effective configuration reproducible.

deployment do
  role :app, user: "deploy"
  role :worker, user: "deploy"

  server "app-01.example.au", roles: %i[app]
  server "worker-01.example.au", roles: %i[worker]

  stage :production do
    server "app-01.example.au", roles: %i[app], user: "release"
  end
end

The production stage replaces the matching app server record rather than adding a duplicate. Role options provide defaults, while server options override them. Resolving one stage must not mutate another, so the stored configuration remains safe to reuse.

A small Ruby grammar for declarations

Implement the grammar as an ordinary Ruby object. Collect declarations first, then translate them into Capistrano calls. This keeps parsing free of SSH activity and gives tests a plain data structure to inspect. A simplified core could look like this:

class RoleDSL
  attr_reader :roles, :servers, :stages, :tasks

  def initialize
    @roles = {}
    @servers = {}
    @stages = {}
    @tasks = {}
  end

  def deployment(&block)
    instance_eval(&block)
  end

  def role(name, **options)
    @roles[name] = options
  end

  def server(host, roles:, **options)
    @servers[host] = { roles: Array(roles), **options }
  end

  def stage(name, &block)
    @stages[name] = RoleDSL.new
    @stages[name].instance_eval(&block)
  end

  def task(name, roles:, &block)
    @tasks[name] = { roles: Array(roles), block: block }
  end
end

The example deliberately uses instance_eval only for trusted, version-controlled configuration. Store task bodies as blocks and execute them later in the Capistrano context, preserving local variables. For configuration loaded from an untrusted source, replace evaluation with a restricted parser.

Avoid inventing a keyword for every Capistrano operation. role, server, stage and task carry distinct deployment meaning; anything else can remain regular Ruby. That small surface makes the DSL easier to learn and easier to remove if it stops earning its keep.

Turning declarations into Capistrano tasks

Capistrano resolves roles from server metadata. The translator can therefore feed each resolved host into config.server, preserving its role list and SSH options:

resolved.servers.each do |host, options|
  config.server host, **options
end

Task definitions need one extra translation. The DSL records which roles own a task, while the Capistrano task chooses the matching servers and runs the original command:

resolved.tasks.each do |name, definition|
  config.task name do
    on roles(*definition.fetch(:roles)) do
      instance_exec(&definition.fetch(:block))
    end
  end
end

Use roles(:app) when every server carrying that role should receive the command. Use release_roles(:app) when the operation must be limited to hosts with a current release. Mixing these up is a common source of apparently random deployment behaviour.

Capistrano already supports roles and stages, so the trade-off is about authoring clarity rather than missing platform features.

Concern Direct Capistrano file DSL-backed configuration
Role membership Read from each server declaration Indexed by role and stage
Stage differences Conditional edits or separate files Explicit blocks with deterministic merging
Task ownership Role names repeated in task bodies Bound once in task metadata
Deployment review Trace statements in file order Inspect the resolved role map

A resolved role-to-host map makes review straightforward: a task can be traced to a capability, and a stage override can be checked against the hosts it affects.

Stage overrides and environment boundaries

Make merging explicit. Start with global roles and servers, apply the selected stage, and let a later stage value replace the matching host or role option. Do not let a stage write back into shared state; otherwise resolving staging can change what production will run.

Store environment-specific facts beside the stage that owns them, but keep secrets out of source control. Read credentials through Capistrano’s fetch helper or environment variables, and leave them out of role membership. A role should describe where a command runs, not prove that a host is authorised to receive it.

Clock handling deserves the same care. A task scheduled for 02:00 in Melbourne can cross a UTC hour or daylight-saving boundary, while a Brisbane host remains on a fixed offset. Convert one explicit schedule to UTC, then test it against hosts in both Australian Eastern locations and Darwin.

Testing the configuration before deployment

Test the configuration as data, not by attempting a deployment. Load the declarations with a fake Capistrano configuration, collect the hosts, roles and task bindings that would be emitted, and assert the resolved result. For example, production should contain the app server exactly once, with the production SSH user applied.

Add negative cases as the DSL grows: an unknown stage, an undeclared role, duplicate membership, or a task bound to a role with no servers. These checks catch configuration mistakes in seconds and produce better errors than a failed SSH command. Keep the resolver deterministic so the same input always produces the same map.

Then run Capistrano’s own checks, including cap deploy:check and a staged task listing with cap -T. The same boundary-first thinking helped me when I documented Composer autoloading notes: ambiguous class resolution became manageable once the conflicting definitions were made explicit. The Ruby analogue is to expose the resolved role map before execution.

Keeping the DSL small and maintainable

Keep the abstraction narrow. It should resolve roles, apply stage overrides and emit Capistrano declarations, not reimplement releases, SSH or task scheduling. Leave difficult deployment logic in ordinary Capistrano Ruby so experienced operators can still recognise it.

Document the merge order, supported Ruby versions and one complete example. Include a command that prints resolved roles without deploying, and make failures name the stage, host and conflicting option. Treat that output as part of the operational runbook, especially when Australian teams share releases across Sydney and Melbourne.

For broader Ruby design patterns, Aaron Orosen’s Ruby guidance is a useful companion. It also helps to keep a small example beside the real configuration: a new role should be understandable without reading the entire parser.

Start with one staging release: define a single role, apply its stage override, and print the resolved role-to-host map before the first task runs. Keep the existing Capistrano tasks intact until the new layer proves that it reduces mistakes rather than merely moving them.

Use the pattern for your next Capistrano release and capture the resolved configuration in deployment logs. If the resulting map is easier to review than the original host list, keep the DSL; otherwise remove it and retain the simpler Ruby.


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