Asgard
A just-like task runner for Ruby. Built on Thor for argument handling and Dagwood for dependency ordering.
The name comes from Norse mythology: Thor is the CLI framework, Asgard is the realm where tasks live, and the task file is named loki — because Loki holds all the tricks.
Installation
gem install asgard
Or add to your Gemfile:
bundle add asgard
Tasks
Every .loki file defines tasks as methods inside class Tasks. The Tasks class is pre-defined by the gem — just reopen it and add methods.
A task with no parameters
class Tasks
desc "hello", "Say hello"
def hello = sh 'echo "Hello, World!"'
end
asgard hello
A task with a positional parameter
Declare positional parameters directly in the method signature. Document them in the desc usage string:
class Tasks
desc "hello NAME", "Say hello to NAME"
def hello(name = "World") = sh "echo 'Hello, #{name}!'"
end
asgard hello
asgard hello Alice
A task with a formal argument declaration
Use argument for richer metadata — type checking, enums, and help text:
class Tasks
argument :name,
type: :string,
default: "World",
desc: "Name to greet"
desc "hello NAME", "Say hello to NAME"
def hello = sh "echo 'Hello, #{name}!'"
end
A task with named options
Use method_option (alias: option) for named flags. Access them inside the method via options[:name]:
class Tasks
desc "hello NAME", "Say hello to NAME"
method_option :shout, aliases: "-s", type: :boolean, desc: "Uppercase the output"
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
def hello(name = "World")
= [:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
[:count].times { sh "echo '#{}'" }
end
end
asgard hello Alice --shout --count 3
A task with an extended description
long_desc provides detailed help shown by asgard help <task>:
class Tasks
long_desc <<~DESC
Says hello to NAME.
Repeats the greeting COUNT times.
Use --shout to uppercase the output.
DESC
desc "hello NAME", "Say hello to NAME"
method_option :shout, aliases: "-s", type: :boolean, desc: "Uppercase the output"
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
def hello(name = "World")
= [:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
[:count].times { sh "echo '#{}'" }
end
end
Dependencies
depends_on declares what must run before a task. Each dependency runs at most once per asgard invocation regardless of how many tasks declare it. Circular dependencies are caught at startup.
desc and depends_on are independent — either can come first, both must appear before def.
Sequential dependencies
Bare symbols run one after another in the order declared:
class Tasks
desc "build", "Compile the project"
def build = sh "rake build"
depends_on :build
desc "test", "Run the test suite"
def test = sh "rake test"
depends_on :test
desc "release", "Publish the gem"
def release = sh "bundle exec rake release"
end
asgard release # build → test → release
Parallel dependencies
Wrap symbols in an array to declare they can run concurrently. Asgard waits for all tasks in a parallel group to finish before moving to the next stage:
class Tasks
desc "lint", "Check code style"
def lint = sh "bundle exec rubocop"
desc "typecheck", "Run type checks"
def typecheck = sh "bundle exec srb tc"
# lint and typecheck run in parallel, test waits for both
depends_on [:lint, :typecheck]
desc "test", "Run the test suite"
def test = sh "bundle exec rake test"
end
asgard test # lint ∥ typecheck → test
Mixed sequential and parallel
Mix bare symbols (sequential) and arrays (parallel) in a single depends_on call. Execution proceeds stage by stage — each stage must complete before the next begins:
class Tasks
desc "setup", "Install dependencies"; def setup = sh "bundle install"
desc "lint", "Check code style"; def lint = sh "bundle exec rubocop"
desc "build", "Compile assets"; def build = sh "rake assets:precompile"
desc "test", "Run tests"; def test = sh "bundle exec rake test"
desc "notify", "Post to Slack"; def notify = sh "curl $SLACK_WEBHOOK -d '{\"text\":\"done\"}'"
# setup first, then lint+build in parallel, then test, then notify
depends_on :setup, [:lint, :build], :test, :notify
desc "ci", "Full CI pipeline"
def ci = sh "echo 'CI complete'"
end
asgard ci executes:
setup
↓
lint ∥ build (concurrent)
↓
test
↓
notify
↓
ci
Variables
var declares a named value available to all tasks as a method. Pass a lambda for lazy evaluation — it is called once on first use:
class Tasks
var :app, "myapp"
var :version, -> { `git describe --tags`.strip }
desc "tag", "Create a release tag"
def tag = sh "git tag #{app}-#{version}"
end
Options shared across all tasks
class_option defines an option available to every task in the class:
class Tasks
class_option :dry_run, aliases: "-n", type: :boolean, desc: "Print commands without running"
desc "deploy ENV", "Deploy to the given environment"
def deploy(env = "staging")
if [:dry_run]
puts "Would deploy to #{env}"
else
sh "cap #{env} deploy"
end
end
end
Shell helpers
sh runs any shell command or multiline heredoc. shebang writes a script body to a tempfile and executes it with the given interpreter. Both exit with the command's status code on failure.
class Tasks
desc "setup", "Bootstrap the development environment"
def setup
sh <<~SHELL
brew install redis postgresql
brew services start redis
bundle install
rails db:setup
SHELL
end
desc "analyze", "Run Python data analysis"
def analyze
shebang :python3, <<~PYTHON
import json
data = json.load(open("results.json"))
print(f"Total: {sum(data.values())}")
PYTHON
end
desc "bundle_assets", "Build frontend assets with esbuild"
def bundle_assets
shebang :node, <<~JS
const esbuild = require("esbuild")
esbuild.buildSync({ entryPoints: ["src/app.js"], bundle: true, outfile: "dist/app.js" })
JS
end
end
Supported interpreters: :python3, :python, :node, :ruby, :perl, :bash, :sh. Any other symbol is passed directly to system with a .tmp extension.
Pass silent: true to suppress the command echo:
def build = sh "rake build", silent: true
Environment variables
dotenv loads a .env file into the environment before tasks run:
class Tasks
dotenv # loads .env
dotenv ".env.local" # or a specific file
desc "check", "Print the app name from .env"
def check = sh "echo $APP_NAME"
end
Command aliases
map creates alternative names for a task:
class Tasks
map "-v" => "version"
map "--v" => "version"
map "t" => "test"
desc "version", "Print the version"
def version = puts Asgard::VERSION
end
method_option types reference
| Type | CLI example | Ruby value |
|---|---|---|
:string |
--branch main |
"main" |
:boolean |
--force / --no-force |
true / false |
:numeric |
--count 3 |
3 |
:array |
--tags foo bar baz |
["foo", "bar", "baz"] |
:hash |
--vars KEY:val FOO:bar |
{"KEY"=>"val", "FOO"=>"bar"} |
Common method_option keys: aliases, type, default, required, desc, enum, banner.
Task files
Asgard searches the current directory and its ancestors for a .loki file. That file marks the project root. All *.loki files in the same directory are auto-loaded alphabetically before .loki is loaded.
Single file
myproject/
.loki
Multiple files
Split tasks across files — each reopens class Tasks:
myproject/
.loki ← entry point, can be empty
build.loki
deploy.loki
test.loki
# build.loki
class Tasks
desc "build", "Compile the project"
def build = sh "rake build"
end
# test.loki
class Tasks
depends_on :build
desc "test", "Run the test suite"
def test = sh "bundle exec rake test"
end
# deploy.loki
class Tasks
depends_on :test
desc "deploy", "Deploy to production"
def deploy = sh "cap production deploy"
end
The .loki entry point can be completely empty — it only needs to exist to mark the project root.
Explicit loading
Load any Ruby or .loki file manually from .loki:
# .loki
require_relative "shared/helpers"
require_relative "ci.loki"
class Tasks
# additional tasks
end
Asgard module API
| Method | Description |
|---|---|
Asgard.run!(argv) |
Entry point — finds .loki, loads task files, starts CLI |
Asgard.find_task_file |
Returns path to .loki searching from CWD upward, or nil |
Asgard.load_loki(dir) |
Loads all *.loki files in dir alphabetically |
run! handles its own errors — a missing .loki or a circular dependency both produce a clean one-line message and exit 1.
Development
git clone git@github.com:MadBomber/asgard.git
cd asgard
bundle install
bundle exec rake test # run tests (95% coverage minimum enforced)
bundle exec bin/asgard help # exercise the CLI against this gem's own .loki
Contributing
Bug reports and pull requests are welcome at https://github.com/MadBomber/asgard.
License
MIT. See LICENSE.txt.