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

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 -> deploy

Common Errors (and the fix)

Pro Tips

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

GoalRuby / shell
Define a tasktask :name do ... end
One prerequisitetask compile: :clean do ... end
Many prerequisitestask build: [:compile, :test]
Namespacenamespace :db do ... end
Describe + listdesc "..." / rake -T
File taskfile "out" => "in" do ... end
Run defaultrake

🎉 Lesson Complete!

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

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.