Defining Tasks¶
A task is a public method inside class Tasks with a desc line above it. That is the whole rule. This page starts there and then adds, one step at a time, everything a task can have: parameters, typed options, extended help, hidden tasks, a default, short aliases, and control over the exit status. Asgard is built on Thor, so Thor's own words (desc, method_option, long_desc, map, default_task, argument) are the ones you write.
Basic Task¶
The method body can be any Ruby. A one-line method reads well with the def name = expression form; a longer one uses the usual def ... end.
Describing a Task: desc¶
desc takes one or two strings.
With one, it is the description shown by asgard help, and the usage line is the method name:
With two, the first is the usage line and the second the description. Use the two-argument form whenever the task takes something after its name, so the help says so:
desc goes directly above the def. depends_on (see Dependencies) goes above the def too, in either order with desc.
The description is one line, shown in the command list. For a longer explanation that asgard help TASK prints under the usage and options, add long_desc; see Extended Help: long_desc below. To describe a task but keep it out of the list, add hide: true; see Hidden Tasks.
Positional Parameters¶
A task's positional parameters are its method's parameters. Give each a default so the task also runs with nothing after its name:
class Tasks
desc "greet NAME", "Greet NAME; omit NAME to greet the world"
def greet(name = "World")
puts "Hello, #{name}!"
end
desc "copy FROM TO", "Copy FROM to TO"
def copy(from, to) = sh "cp #{from} #{to}"
end
asgard greet # Hello, World!
asgard greet Alice # Hello, Alice!
asgard copy a.txt b.txt
asgard copy a.txt
A missing required parameter is a usage error: the message prints and asgard exits 1.
Dependencies cannot take required parameters
A task that another task lists in depends_on is run with no arguments. If it has a required parameter, asgard refuses at startup: asgard: task 'copy' has 2 required argument(s) and cannot be used as a dependency. Give the parameter a default, read the value from an option or an environment variable, or depend on a task that calls it with fixed arguments; see A Dependency With Arguments.
A splat collects everything that follows:
desc "lint FILES...", "Lint the given files (default: lib)"
def lint(*files)
files = ["lib"] if files.empty?
sh "rubocop #{files.join(' ')}"
end
Named Options¶
option (or its longer name method_option) declares a flag for the task that follows it. Inside the task, options.name or options[:name] holds the value, and a boolean reads as options.name?.
class Tasks
desc "Compile the project"
option :output, aliases: "-o", type: :string, default: "dist/", desc: "Output directory"
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Enable verbose output"
option :jobs, aliases: "-j", type: :numeric, default: 1, desc: "Number of parallel jobs"
option :tags, type: :array, desc: "Build tags to apply"
option :defines, type: :hash, desc: "Preprocessor defines (KEY:VALUE)"
def compile
puts "Compiling into #{options.output} with #{options.jobs} job(s)"
puts "Tags: #{options.tags.join(', ')}" if options.tags
puts "Defines: #{options.defines}" if options.defines
puts "(verbose)" if options.verbose?
end
end
Compiling into build/ with 4 job(s)
Tags: debug, release
Defines: {"VERSION" => "2", "MODE" => "fast"}
Reading an Option¶
options answers three spellings, all the same value:
| Spelling | Returns |
|---|---|
options.jobs |
The value, or nil when the option was not given and has no default |
options[:jobs] |
The same; a string key, options["jobs"], works too |
options.verbose? |
true or false, never nil; the natural form for a boolean |
A predicate with an argument compares: options.format?("md") is options[:format] == "md".
The Five Option Types¶
| Type | On the command line | Value in the task |
|---|---|---|
:string |
--output dist/ or --output=dist/ |
"dist/" |
:boolean |
--verbose / --no-verbose |
true / false |
:numeric |
--jobs 4 |
4 |
:array |
--tags foo bar baz |
["foo", "bar", "baz"] |
:hash |
--defines KEY:val FOO:bar |
{"KEY" => "val", "FOO" => "bar"} |
An option with no type: is a string. Every boolean also accepts its --no- form, and --skip-name means the same as --no-name.
Option Keys¶
| Key | Meaning |
|---|---|
aliases |
A short form, "-o", or several, %w[-o --out] |
type |
One of the five types above |
default |
The value when the flag is not given |
required |
true makes omitting the flag a usage error |
desc |
The one-line description shown in help |
enum |
The allowed values; anything else is a usage error |
banner |
The placeholder shown in help for the value, instead of the type name |
A Required Option¶
class Tasks
desc "deploy ENV", "Deploy to ENV"
option :strategy, type: :string, required: true, enum: %w[blue-green rolling canary], desc: "Deployment strategy"
def deploy(env = "staging")
sh "cap #{env} deploy --strategy #{options[:strategy]}"
end
end
Validating with enum¶
class Tasks
desc "Build the project"
option :env, type: :string, default: "development", enum: %w[development staging production], desc: "Target environment"
def build = sh "rake build ENV=#{options[:env]}"
end
A Clearer Placeholder with banner¶
class Tasks
desc "Wait for a service to come up"
option :timeout, type: :numeric, default: 30, banner: "SECONDS", desc: "Give up after SECONDS"
def wait = sh "wait-for-it localhost:3000 --timeout #{options[:timeout]}"
end
asgard help wait shows [--timeout=SECONDS] instead of [--timeout=N].
Options that every task should accept are declared once with class_option; see Options & Flags.
Extended Help: long_desc¶
long_desc adds a longer explanation that asgard help <task> prints after the usage and options. The text is re-wrapped to the terminal width; put \x5 at the end of a line to force a line break there, which keeps example commands on their own lines:
class Tasks
long_desc <<~DESC
Generates a project report covering test coverage, lint results,
and a dependency audit.
Pass --format to control the output style. Use --since to limit the
report to changes after a given date.
Examples:\x5
asgard report --format html --since 2024-01-01\x5
asgard report --format json --output report.json\x5
asgard report --format text
DESC
desc "Generate a project report"
option :format, type: :string, default: "text", enum: %w[text html json], desc: "Output format"
option :since, type: :string, banner: "DATE", desc: "Limit to changes after DATE"
option :output, type: :string, banner: "FILE", desc: "Write the report to FILE"
def report = sh "generate-report --format #{options[:format]}"
end
Usage:
asgard report
Options:
[--format=FORMAT] # Output format
# Default: text
# Possible values: text, html, json
[--since=DATE] # Limit to changes after DATE
[--output=FILE] # Write the report to FILE
...
Description:
Generates a project report covering test coverage, lint results, and a
dependency audit.
Pass --format to control the output style. Use --since to limit the report
to changes after a given date.
Examples:
asgard report --format html --since 2024-01-01
asgard report --format json --output report.json
asgard report --format text
long_desc, desc, the options and depends_on can appear in any order; they all apply to the next def.
Hidden Tasks¶
A hidden task runs from the command line and can be a dependency, but asgard help leaves it out. There are two ways to get one.
Ask for it with hide: true. Use this for a task that is part of the project's machinery and not something a person would type:
class Tasks
desc "Ask before running the task that depends on this", hide: true
def confirm
answer = ask("Really run `asgard #{ARGV.join(' ')}`? [y/N]")
raise Asgard::Error, "aborted" unless answer.strip.downcase.start_with?("y")
end
depends_on :confirm
desc "Drop and recreate the database"
def reset_database = sh "bin/rails db:reset"
end
asgard help shows reset_database but not confirm; asgard confirm still works.
Leave out the desc. A public method with no desc is also a hidden task:
Helpers must not be public methods
This catches people who write helper methods as plain defs. Every public method is a task: asgard current_sha runs it, depends_on :current_sha accepts it, and asgard --doctor lists it as hidden (no desc; not shown by help). Make helpers private, put them inside no_commands do ... end, or define them with helper. All three are on Helper Methods.
Default Task¶
default_task names the task that runs when you type asgard with nothing after it:
class Tasks
default_task :greet
desc "Say hello (runs by default)"
def greet = puts "Hello from Asgard!"
end
Without default_task, a bare asgard prints the help.
Only one default can win. When a second file sets another, asgard warns on standard error and the last one loaded is used:
asgard: default_task :deploy at /proj/deploy.loki:3 overrides default_task :greet set at /proj/.loki:2
Abbreviated Command Matching¶
Every task is a Thor command, and Thor resolves any unambiguous prefix of a command name to that command automatically. Nothing to declare. Given:
class Tasks
desc "Check code style with RuboCop"
def rubocop_check = sh "bundle exec rubocop"
desc "Run the test suite"
def test_check = sh "bundle exec rake test"
desc "Run the test suite with verbose output"
def test_verbose = sh "bundle exec rake test -v"
desc "Deploy to production"
def deploy = sh "cap production deploy"
desc "Deploy to staging"
def deploy_staging = sh "cap staging deploy"
end
asgard r # same as: asgard rubocop_check, the only task starting with "r"
asgard test_c # same as: asgard test_check, enough of the name to be unique
asgard test # Ambiguous command test matches [test_check, test_verbose]
asgard deploy # runs deploy, not deploy_staging; see below
Matching is on a plain prefix, so the shortest string that is still unique among your tasks works. Two details worth knowing:
- An exact full name always wins, even if it is also a prefix of another task.
asgard deployrunsdeployitself, never the ambiguous-prefix error, becausedeployis a defined command and not merely a prefix ofdeploy_staging. - An ambiguous prefix produces a clean error listing every candidate (
Ambiguous command X matches [...]) rather than guessing or running the first match. Type enough of the name to disambiguate.
Tip
Because matching depends on every task name currently defined, a short prefix that is unique today can become ambiguous tomorrow when a new task with the same stem is added. Adding rubocop_fix alongside rubocop_check turns asgard r from a clean match into Ambiguous command r matches [rubocop_check, rubocop_fix]. Use map for a short name that must stay stable.
Command Aliases¶
map gives a task another name. Several aliases can point at one task:
class Tasks
map "t" => "test"
map "b" => "build"
map "-v" => "version"
desc "Print the project version"
def version = puts "1.4.0"
desc "Run tests"
def test = sh "bundle exec rake test"
desc "Build the gem"
def build = sh "gem build myapp.gemspec"
end
asgard t # same as: asgard test
asgard b # same as: asgard build
asgard -v # same as: asgard version (asgard's own flag is --version)
An alias does not appear in asgard help; the task it points at does.
Return Values and Exit Status¶
What a task returns decides asgard's exit status, which is what a shell script or CI job sees.
| The task | Exit status |
|---|---|
Returns :fail |
1 |
Returns anything else (nil, true, :warn, :skip, a string) |
0 |
Raises Asgard::Error, "message" |
1, after printing asgard: message |
Runs an sh command that fails |
The command's exit status; asgard stops at that command |
Calls abort "message" |
1, after printing the message |
The :fail convention suits a check that reports its own result and should not stop other work:
class Tasks
desc "Check code style with RuboCop"
def rubocop_check
output = `bundle exec rubocop 2>&1`
File.write("rubocop_output.txt", output)
puts "RuboCop: #{output.lines.find { |l| l.include?('files inspected') }&.strip} (see rubocop_output.txt)"
$?.success? ? :pass : :fail
end
end
raise Asgard::Error is for a task that must stop everything with a message and no backtrace:
desc "Release the gem"
def release
raise Asgard::Error, "working tree is not clean" unless `git status --porcelain`.empty?
sh "gem push pkg/myapp.gem"
end
Note
What a failing dependency means is up to you. A dependency's return value never stops the task that depends on it: if rubocop_check returns :fail as a dependency of build, build still runs and can read the result. A dependency that must succeed stops the chain itself, with abort, a non-zero exit, a failing sh or raise Asgard::Error. Both patterns are covered on Dependencies.
Reserved Names¶
Some names belong to the machinery that runs your tasks, and a task or helper by one of them would break every command. Asgard refuses them when the .loki loads, naming the file and line:
The reserved names are the ones Thor uses internally, among them run, shell, options, invoke, args, action, behavior, root, inside, in_root, create_file, add_file and run_ruby_script, and the helpers asgard gives every task body: sh, shebang, env, import, import_up, loki_up, changes, debug?, verbose?, dep_result, dep_results, current_task and invoke_command. A task named sh would otherwise replace the helper, and every other task's sh would quietly run it instead:
Pick another word (run_all, start, shell_in) or put the task in a subcommand.
Names starting with _ are asgard's own. Asgard's built-in commands use them, and asgard _anything is refused with asgard: unknown command '_anything'. Do not start a task or helper name with an underscore.
Formal Argument Declaration¶
argument declares a positional parameter with metadata: a type, a default, allowed values and a description that shows in help.
Class-level scope
argument applies to every task in the class, not just the one that follows it. It suits a single-command tool, or a class whose tasks all take the same first value. In a .loki with many tasks, use method parameters instead.
class Tasks
argument :name, type: :string, default: "World", desc: "Name to greet"
desc "hello NAME", "Say hello to NAME"
def hello = puts "Hello, #{name}!"
end
For a multi-task file, the same thing as a method parameter:
Running Several Tasks in One Command¶
asgard runs one task per invocation, plus its dependencies. The do task in the repository's examples adds the asgard do lint test build style on top, as an ordinary task you can copy into your own .loki; steps can carry their own arguments when separated by commas. See Examples.
Helpers¶
Not every method should be a task. private methods, no_commands blocks and the helper DSL all define methods your tasks can call that never show up as commands. They are covered on Helper Methods.