Automation with Rake
Reviewed & published by Brayan K
Rake is Ruby's built-in task runner: you define named tasks and their dependencies in a Rakefile, then run them from the command line to automate building, testing, and other repetitive jobs.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll write tasks with dependencies and namespaces, and see exactly how Rake decides what to run and in what order.
What You'll Learn in This Lesson
- Writing a Rakefile with task :name do...end
- Declaring dependencies with task a: :b
- How Rake runs prerequisites first, once each
- Grouping tasks with namespaces
- Documenting tasks with desc and rake -T
- File tasks that skip when up to date
1️⃣ Anatomy of a Rakefile
A Rakefile (no extension) holds task :name do ... end blocks. The hash form task build: [:compile, :test] declares prerequisites, namespace groups tasks, and desc documents them. Here's a complete one you can save and run:
2️⃣ How Dependency Resolution Works
When you run a task, Rake walks its prerequisite graph depth-first, running each prerequisite before the task itself, and never running the same task twice. The runnable simulation below recreates that algorithm in plain Ruby so you can watch the order emerge.
# Rake resolves task DEPENDENCIES: it runs prerequisites first, once each.
# Here's a tiny simulation in plain Ruby so you can SEE the order.
tasks = {
build: { deps: [:compile, :test], action: -> { puts "build: packaging app" } },
compile:{ deps: [:clean], action: -> { puts "compile: compiling sources" } },
test: { deps: [:compile], action: -> { puts "test: running tests" } },
clean: { deps: [], action: -> { puts "clean: removing old files" } },
}
done = []
run = lambda do |name|
return if done.include?(name) # each task runs at most once
tasks[name][:deps].each { |dep| run.call(dep) } # prerequisites first
tasks[name][:action].call
done << name
end
run.call(:build)
puts "Order: #{done.join(' -> ')}"3️⃣ Namespaces and rake -T
Namespaces prefix task names (db:migrate) to keep large projects tidy, and resolution still chains through them. Meanwhile rake -T prints every task that has a desc, neatly aligned — a self-documenting menu. The two simulations below reproduce both.
# Namespaces group related tasks. Rake refers to them as "db:migrate" etc.
# Simulation: a namespaced task registry resolving "db:reset".
registry = {
"db:drop" => { deps: [], run: -> { puts "Dropping database" } },
"db:create" => { deps: ["db:drop"], run: -> { puts "Creating database" } },
"db:migrate" => { deps: ["db:create"], run: -> { puts "Running migrations" } },
"db:reset" => { deps: ["db:migrate"],run: -> { puts "Reset complete" } },
}
done = []
invoke = lambda do |name|
return if done.include?(name)
registry[name][:deps].each { |d| invoke.call(d) }
registry[name][:run].call
done << name
end
invoke.call("db:reset")
puts done.inspect# `rake -T` lists tasks that have a `desc`. Simulation of that listing.
catalog = [
{ name: "build", desc: "Compile, test, and package the app" },
{ name: "test", desc: "Run the test suite" },
{ name: "db:migrate", desc: "Apply pending database migrations" },
{ name: "clean", desc: nil }, # no desc -> hidden from rake -T
]
# rake -T shows only described tasks, aligned, prefixed with "rake".
width = catalog.select { |t| t[:desc] }.map { |t| t[:name].length }.max
catalog.each do |t|
next unless t[:desc] # undescribed tasks are omitted
puts "rake #{t[:name].ljust(width)} # #{t[:desc]}"
end🎯 Your Turn
The resolver needs to read each task's prerequisites. Replace the ___ with the hash key that holds the dependency list.
# 🎯 YOUR TURN — fill in the ___ so prerequisites run BEFORE the task.
tasks = {
deploy: { deps: [:build], action: -> { puts "deploy: shipping it" } },
build: { deps: [:test], action: -> { puts "build: assembling" } },
test: { deps: [], action: -> { puts "test: passing" } },
}
done = []
run = lambda do |name|
return if done.include?(name)
tasks[name][:___].each { |dep| run.call(dep) } # 👉 which key holds prerequisites?
tasks[name][:action].call
done << name
end
run.call(:deploy)
puts done.join(" -> ")
# ✅ Expected output:
# test: passing
# build: assembling
# deploy: shipping it
# test -> build -> deployCommon Errors (and the fix)
- "Don't know how to build task" — ❌ Typo or missing task name. ✅ Check spelling and that the task is defined in the Rakefile.
- desc applies to the wrong task — ❌ desc must sit directly above its task. ✅ Put them on consecutive lines.
- Namespaced task not found — ❌ You ran rake migrate. ✅ Use the full name rake db:migrate.
- Prerequisite runs every time — ❌ You expected file-task skipping on a plain task. ✅ Use a file task with a target for timestamp checks.
- Circular dependency — ❌ Task A needs B and B needs A. ✅ Break the cycle; Rake raises an error on circular graphs.
Pro Tips
- 💡 Always add a desc: it turns rake -T into living documentation of your project.
- 💡 Set a default: task default: :test so a bare rake does the common thing.
- 💡 Pass arguments: task :greet, [:name] do |t, args| ... end lets you call rake "greet[Ada]".
Mini-Challenge: Resolve a Diamond Dependency
Build a resolver for a graph where clean is a prerequisite of both compile and assets — yet must run only once. Run with ruby graph.rb.
# 🎯 MINI-CHALLENGE: resolve a task graph with a SHARED prerequisite.
# Save as graph.rb and run with: ruby graph.rb
#
# graph = { release: [:compile, :assets], compile: [:clean],
# assets: [:clean], clean: [] }
#
# "clean" is needed by both compile and assets — but it must run ONLY ONCE.
# Walk the graph depth-first, recording each task the first time it completes.
#
# ✅ Expected output:
# clean -> compile -> assets -> release
# Tasks run: 4
# your code here📋 Quick Reference — Rake
| Goal | Ruby / shell |
|---|---|
| Define a task | task :name do ... end |
| One prerequisite | task compile: :clean do ... end |
| Many prerequisites | task build: [:compile, :test] |
| Namespace | namespace :db do ... end |
| Describe + list | desc "..." / rake -T |
| File task | file "out" => "in" do ... end |
| Run default | rake |
🎉 Lesson Complete!
- ✅ A Rakefile holds task :name do ... end blocks
- ✅ task a: :b declares dependencies; Rake runs them first, once each
- ✅ namespace groups tasks into db:migrate-style names
- ✅ desc + rake -T document your tasks
- ✅ file tasks skip work that's already up to date
Practice quiz
What is Rake?
- A Ruby web framework
- A testing assertion library
- Ruby's build tool and task runner
- A package manager
Answer: Ruby's build tool and task runner. Rake is Ruby's build tool and task runner; its name comes from 'Ruby make'.
What is the conventional filename Rake looks for?
- Rakefile
- tasks.rb
- rake.yml
- Gemfile
Answer: Rakefile. Rake reads tasks from a file named Rakefile (no extension).
How do you define a basic task?
- def :name do ... end
- rake :name { ... }
- namespace :name do ... end
- task :name do ... end
Answer: task :name do ... end. A task is declared with task :name do ... end.
How do you declare that build depends on compile and test?
- task :build, :compile, :test
- task build: [:compile, :test]
- depends build => compile, test
- task build needs compile test
Answer: task build: [:compile, :test]. The hash form task build: [:compile, :test] declares an array of prerequisites.
If two tasks both depend on clean, how many times does clean run per invocation?
- At most once
- Once per dependent
- Twice
- It depends on order
Answer: At most once. Rake runs each task at most once per invocation, even when shared.
How do you invoke a task defined inside namespace :db as :migrate?
- rake migrate
- rake db.migrate
- rake db:migrate
- rake :db:migrate
Answer: rake db:migrate. Namespaced tasks are invoked with the prefix, e.g. rake db:migrate.
What does the desc directive do?
- Deletes a task
- Attaches a one-line description shown by rake -T
- Declares a dependency
- Marks the default task
Answer: Attaches a one-line description shown by rake -T. desc documents the following task; rake -T lists described tasks.
Which command lists all tasks that have a desc?
- rake -A
- rake list
- rake --all
- rake -T
Answer: rake -T. rake -T prints every described task with its summary.
When does a file task run its action?
- Always when invoked
- Only if the target is missing or out of date
- Only once ever
- Never automatically
Answer: Only if the target is missing or out of date. A file task runs only if the target doesn't exist or a prerequisite is newer.
What does task default: :build accomplish?
- Renames build
- Hides build from rake -T
- Makes a bare 'rake' run build
- Disables build
Answer: Makes a bare 'rake' run build. It sets the default task, so running rake with no args runs build.
Continue this course
- Previous: Threads, Mutex & Ractor
- Next: CLI Scripts with OptionParser — Parse ARGV into clean options for real command-line tools
- Quick reference: Ruby cheat sheet
Frequently asked questions
What is Rake and why use it?
Rake is Ruby's build tool and task runner — its name comes from 'Ruby make'. You write small named tasks in a file called Rakefile, then run them from the command line with rake task_name. It is perfect for automating the repetitive jobs around a project: running tests, building releases, resetting a database, generating files. Because tasks are written in plain Ruby, you have the full language available, which makes Rake far more flexible than a shell script for anything non-trivial.
How do task dependencies work in Rake?
You declare prerequisites with the hash form task build: [:compile, :test]. Before Rake runs build, it runs compile and test first — and any of their prerequisites, recursively. Crucially, each task runs at most once per invocation, even if several tasks depend on it, so a shared 'clean' step doesn't run twice. This dependency resolution is the heart of Rake: you describe what depends on what, and Rake figures out a correct order to run everything.
What are namespaces and the desc directive for?
A namespace groups related tasks under a prefix, so namespace :db { task :migrate } is invoked as rake db:migrate — this keeps large Rakefiles organised and avoids name clashes. The desc directive attaches a one-line description to the task that follows it. Descriptions matter because rake -T lists every described task with its summary, giving anyone a self-documenting menu of what the project can do. Tasks without a desc are hidden from that listing.
What is a file task and how is it different from a regular task?
A file task names a file as its target — file 'out.txt' => 'in.txt' do ... end. Rake runs its action only if the target doesn't exist yet, or if a prerequisite file is newer than the target. This timestamp-based skipping is exactly how the classic make tool works and is ideal for build pipelines: you avoid regenerating outputs that are already up to date. A regular task, by contrast, always runs its action when invoked, regardless of any files.