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.
ls -la ~/languages/
- drwxr-xr-x
- ▶ PHP
- ▶ Ruby
- ▶ Scala
- ▶ C#
- ▶ JavaScript
- ▶ Objective-C
- ▶ Shell Scripting
ls -la ~/toolchain/
- drwxr-xr-x
- ▶ Typo3
- ▶ Akka
- ▶ Capistrano
- ▶ Git
- ▶ MAMP
- ▶ Adobe Illustrator
- ▶ NSTrackingArea (Cocoa)
uname -a
- 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
| Key | Value |
|---|---|
| editor | Terminal-first workflow |
| os | Mac OS X / Unix |
| vcs | Git, distributed version control |
| deploy | Capistrano, cron automation |
| graphics | SVG, Adobe Illustrator troubleshooting |
| networking | IP validation, SSH, VPN |
git log --oneline --reverse
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.
Solving NDK build issues on OS X
Troubleshooting native development kit compilation problems on Mac OS X.
Programmatically adding PHP generated TypoScript to the backend configuration
Integrating dynamically generated TypoScript into Typo3 backend setups using PHP.
ArgumentError: Could not parse PKey: no start line
Debugging an SSH key parsing error encountered during deployment.
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.
Cocoa: Using NSTrackingArea
A short tutorial on using Cocoa's NSTrackingArea to capture mouseEntered and mouseExited events.
cat ~/contact.txt