asgard 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +179 -99
- data/README.md +141 -46
- data/bin/asgard +10 -0
- data/lib/asgard/base/dispatch.rb +257 -31
- data/lib/asgard/base/registry.rb +7 -3
- data/lib/asgard/base.rb +140 -9
- data/lib/asgard/declarations.rb +21 -0
- data/lib/asgard/doctor/task_sections.rb +11 -3
- data/lib/asgard/doctor.rb +12 -8
- data/lib/asgard/jobs/commands.rb +152 -0
- data/lib/asgard/jobs/declaration.rb +149 -0
- data/lib/asgard/jobs/launcher.rb +109 -0
- data/lib/asgard/jobs/state.rb +190 -0
- data/lib/asgard/jobs/stopper.rb +141 -0
- data/lib/asgard/jobs.rb +25 -0
- data/lib/asgard/kernel_methods.rb +19 -14
- data/lib/asgard/leading_flag.rb +10 -0
- data/lib/asgard/pid.rb +130 -0
- data/lib/asgard/schedule/commands.rb +39 -11
- data/lib/asgard/schedule/declaration.rb +139 -12
- data/lib/asgard/schedule/job_files.rb +24 -0
- data/lib/asgard/schedule/launchd.rb +55 -18
- data/lib/asgard/schedule/systemd.rb +69 -27
- data/lib/asgard/schedule/table.rb +34 -16
- data/lib/asgard/schedule.rb +49 -9
- data/lib/asgard/shell.rb +372 -44
- data/lib/asgard/state_dir.rb +84 -0
- data/lib/asgard/tasks.rb +8 -0
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard/watch/changes.rb +26 -1
- data/lib/asgard/watch/commands.rb +52 -12
- data/lib/asgard/watch/daemon.rb +334 -43
- data/lib/asgard/watch/declaration.rb +135 -40
- data/lib/asgard/watch/dispatcher.rb +61 -10
- data/lib/asgard/watch/mailbox.rb +50 -0
- data/lib/asgard/watch/reporter.rb +50 -34
- data/lib/asgard/watch/spawn.rb +41 -16
- data/lib/asgard/watch/state.rb +93 -27
- data/lib/asgard/watch/worker.rb +44 -9
- data/lib/asgard/watch.rb +4 -11
- data/lib/asgard.rb +80 -14
- metadata +167 -186
- data/.envrc +0 -6
- data/.github/workflows/deploy-github-pages.yml +0 -52
- data/.loki +0 -39
- data/.reek.yml +0 -166
- data/.rubocop.yml +0 -163
- data/Archspec.rb +0 -24
- data/CLAUDE.md +0 -139
- data/COMMITS.md +0 -196
- data/bin/console +0 -11
- data/bin/setup +0 -8
- data/doc_tasks.loki +0 -21
- data/docs/api.md +0 -368
- data/docs/assets/css/custom.css +0 -93
- data/docs/assets/images/asgard.jpg +0 -0
- data/docs/changelog.md +0 -1
- data/docs/dependencies.md +0 -313
- data/docs/environment.md +0 -129
- data/docs/examples.md +0 -194
- data/docs/getting-started.md +0 -181
- data/docs/helpers.md +0 -262
- data/docs/index.md +0 -87
- data/docs/options.md +0 -224
- data/docs/schedule.md +0 -106
- data/docs/shell.md +0 -237
- data/docs/subcommands.md +0 -181
- data/docs/task-files.md +0 -407
- data/docs/tasks.md +0 -326
- data/docs/variables.md +0 -338
- data/docs/watch.md +0 -123
- data/examples/.env +0 -4
- data/examples/.envrc +0 -3
- data/examples/.loki +0 -24
- data/examples/bad.loki +0 -63
- data/examples/concurrent.loki +0 -58
- data/examples/confirm.loki +0 -48
- data/examples/db_subcommands.loki +0 -74
- data/examples/depends_on_block/bad/.loki +0 -51
- data/examples/depends_on_block/good/.loki +0 -83
- data/examples/do.loki +0 -45
- data/examples/env_usage.loki +0 -27
- data/examples/kitchen_sink.loki +0 -210
- data/examples/server_subcommands.loki +0 -65
- data/examples/subdir/.loki +0 -12
- data/examples/subdir/import_demo.loki +0 -14
- data/examples/subdir/import_up_demo.loki +0 -18
- data/examples/watcher_examples/.envrc +0 -3
- data/examples/watcher_examples/.loki +0 -54
- data/examples/watcher_examples/README.md +0 -112
- data/examples/watcher_examples/bin/hello +0 -3
- data/examples/watcher_examples/data/records.json +0 -1
- data/examples/watcher_examples/docs/drafts/ideas.md +0 -5
- data/examples/watcher_examples/docs/index.md +0 -4
- data/examples/watcher_examples/lib/greeter.rb +0 -7
- data/gem_tasks.loki +0 -51
- data/git.loki +0 -13
- data/mkdocs.yml +0 -166
- data/quality.loki +0 -266
- data/quality_rails.loki +0 -47
- data/xyzzy.loki +0 -12
data/docs/options.md
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
# Options & Flags
|
|
2
|
-
|
|
3
|
-
Asgard tasks use the full Thor option system. Options declared with `method_option` (alias: `option`) apply to a single task. Options declared with `class_option` apply to every task in the class. Asgard ships with four built-in `class_option` declarations on `Tasks`: `--debug`, `--verbose`, `--version`, and `--doctor`.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Per-Task Options
|
|
8
|
-
|
|
9
|
-
`method_option` (or its alias `option`) declares an option for the immediately following task:
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
class Tasks
|
|
13
|
-
desc "deploy ENV", "Deploy to ENV"
|
|
14
|
-
method_option :branch,
|
|
15
|
-
aliases: "-b",
|
|
16
|
-
type: :string,
|
|
17
|
-
default: "main",
|
|
18
|
-
desc: "Git branch to deploy"
|
|
19
|
-
method_option :dry_run,
|
|
20
|
-
aliases: "-n",
|
|
21
|
-
type: :boolean,
|
|
22
|
-
default: false,
|
|
23
|
-
desc: "Print commands without running"
|
|
24
|
-
def deploy(env = "staging")
|
|
25
|
-
if options[:dry_run]
|
|
26
|
-
puts "Would deploy #{options[:branch]} to #{env}"
|
|
27
|
-
else
|
|
28
|
-
sh "cap #{env} deploy BRANCH=#{options[:branch]}"
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Access option values inside the task body via `options[:name]` (a hash keyed by symbol).
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## Class Options (Shared Across All Tasks)
|
|
39
|
-
|
|
40
|
-
`class_option` defines an option available on every task in the class. Add your own to complement the built-in ones:
|
|
41
|
-
|
|
42
|
-
```ruby
|
|
43
|
-
class Tasks
|
|
44
|
-
class_option :dry_run,
|
|
45
|
-
aliases: "-n",
|
|
46
|
-
type: :boolean,
|
|
47
|
-
default: false,
|
|
48
|
-
desc: "Print commands without running"
|
|
49
|
-
|
|
50
|
-
class_option :env,
|
|
51
|
-
type: :string,
|
|
52
|
-
default: "development",
|
|
53
|
-
enum: %w[development staging production],
|
|
54
|
-
desc: "Target environment"
|
|
55
|
-
|
|
56
|
-
desc "Deploy the application"
|
|
57
|
-
def deploy
|
|
58
|
-
if options[:dry_run]
|
|
59
|
-
puts "Would deploy to #{options[:env]}"
|
|
60
|
-
else
|
|
61
|
-
sh "cap #{options[:env]} deploy"
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
desc "Run database migrations"
|
|
66
|
-
def migrate
|
|
67
|
-
sh "rails db:migrate RAILS_ENV=#{options[:env]}"
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Both `deploy` and `migrate` automatically accept `--dry-run` and `--env`.
|
|
73
|
-
|
|
74
|
-
### Boolean class options and `no_negate`
|
|
75
|
-
|
|
76
|
-
Thor automatically generates `[--no-name]` and `[--skip-name]` help entries alongside every boolean `class_option`. When negation is meaningful — `--dry-run` paired with `--no-dry-run` — this is useful. When negation is meaningless, call `no_negate` immediately after the declaration to remove the extra variants from the help output:
|
|
77
|
-
|
|
78
|
-
```ruby
|
|
79
|
-
class Tasks
|
|
80
|
-
class_option :color,
|
|
81
|
-
type: :boolean,
|
|
82
|
-
default: true,
|
|
83
|
-
desc: "Colorize output"
|
|
84
|
-
no_negate :color
|
|
85
|
-
end
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Help output before `no_negate`:
|
|
89
|
-
|
|
90
|
-
```
|
|
91
|
-
[--color], [--no-color], [--skip-color] # Colorize output
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Help output after `no_negate`:
|
|
95
|
-
|
|
96
|
-
```
|
|
97
|
-
[--color] # Colorize output
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`no_negate` accepts multiple option names in a single call:
|
|
101
|
-
|
|
102
|
-
```ruby
|
|
103
|
-
no_negate :color, :version, :emoji
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
It has no effect on runtime behaviour — `--no-color` still works on the CLI; only the help display is affected.
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Built-in Flags
|
|
111
|
-
|
|
112
|
-
`Tasks` ships with four built-in `class_option` declarations — `--debug`, `--verbose`, `--version`, and `--doctor` — all visible in the Options section of `asgard help`.
|
|
113
|
-
|
|
114
|
-
### `--version`
|
|
115
|
-
|
|
116
|
-
A `class_option :version` of type `:boolean`. Prints `Asgard::VERSION` and exits. Handled by `Asgard.run!` before the `.loki` file is loaded, so it works even in a directory without a `.loki` file. `no_negate :version` suppresses the `[--no-version]` / `[--skip-version]` variants (see [Boolean class options and `no_negate`](#boolean-class-options-and-no_negate) above):
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
asgard --version
|
|
120
|
-
# 0.3.0
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### `--doctor`
|
|
124
|
-
|
|
125
|
-
A `class_option :doctor` of type `:boolean`. Diagnoses `.loki` resolution, import chains, and task definitions for the current directory, then exits. Handled by `Asgard.run!` before the `.loki` file is loaded (same pattern as `--version`) — deliberately so, since it needs to keep working in the exact situations that would otherwise abort `run!`: a broken `.loki` file, a circular or undefined dependency, or a task silently redefined by a later `def`. The report includes a "Tasks by file" listing — every command grouped by the file it's defined in, as `file:line` — with any silently-overridden task called out inline, right where it's defined. `no_negate :doctor` suppresses the `[--no-doctor]` / `[--skip-doctor]` variants:
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
asgard --doctor
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
See [`Asgard::Doctor` in the API Reference](api.md#asgarddoctor) for the full breakdown of what it checks.
|
|
132
|
-
|
|
133
|
-
### `--debug`
|
|
134
|
-
|
|
135
|
-
A `class_option :debug` of type `:boolean`. When passed, sets `$DEBUG = true` before the task body runs (via the `invoke_command` hook in `Asgard::Base`):
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
asgard build --debug
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Inside the task, use the `debug?` predicate:
|
|
142
|
-
|
|
143
|
-
```ruby
|
|
144
|
-
def build
|
|
145
|
-
sh "rake build"
|
|
146
|
-
sh "rake build --trace" if debug?
|
|
147
|
-
end
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### `--verbose`
|
|
151
|
-
|
|
152
|
-
A `class_option :verbose` of type `:boolean`. When passed, sets `$VERBOSE = true` before the task body runs:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
asgard test --verbose
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Inside the task, use the `verbose?` predicate:
|
|
159
|
-
|
|
160
|
-
```ruby
|
|
161
|
-
def test
|
|
162
|
-
flags = verbose? ? "--verbose" : ""
|
|
163
|
-
sh "bundle exec rake test #{flags}"
|
|
164
|
-
end
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
---
|
|
168
|
-
|
|
169
|
-
## `debug?` and `verbose?` Predicates
|
|
170
|
-
|
|
171
|
-
Both are private methods on `Tasks`, thin wrappers around the global variables:
|
|
172
|
-
|
|
173
|
-
```ruby
|
|
174
|
-
private
|
|
175
|
-
|
|
176
|
-
def debug? = $DEBUG
|
|
177
|
-
def verbose? = $VERBOSE
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
They are available in every task body and in subcommand classes that inherit from `Tasks`. Because `--debug` and `--verbose` are `class_option` declarations (not standalone commands), they work as modifiers alongside any task:
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
asgard build --debug --verbose
|
|
184
|
-
asgard deploy production --verbose
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
## Option Types Reference
|
|
190
|
-
|
|
191
|
-
| Type | CLI Example | Ruby Value |
|
|
192
|
-
|---|---|---|
|
|
193
|
-
| `:string` | `--branch main` | `"main"` |
|
|
194
|
-
| `:boolean` | `--force` / `--no-force` | `true` / `false` |
|
|
195
|
-
| `:numeric` | `--count 3` | `3` |
|
|
196
|
-
| `:array` | `--tags foo bar baz` | `["foo", "bar", "baz"]` |
|
|
197
|
-
| `:hash` | `--vars KEY:val FOO:bar` | `{"KEY"=>"val", "FOO"=>"bar"}` |
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## Option Keys Reference
|
|
202
|
-
|
|
203
|
-
| Key | Applies to | Description |
|
|
204
|
-
|---|---|---|
|
|
205
|
-
| `aliases` | `method_option`, `class_option` | Short-form flag string, e.g. `"-b"` |
|
|
206
|
-
| `type` | `method_option`, `class_option` | One of the five types above |
|
|
207
|
-
| `default` | `method_option`, `class_option` | Value used when the flag is omitted |
|
|
208
|
-
| `required` | `method_option` | Raises an error if the flag is missing |
|
|
209
|
-
| `desc` | `method_option`, `class_option` | One-line description shown in help |
|
|
210
|
-
| `enum` | `method_option`, `class_option` | Allowed values; validated by Thor |
|
|
211
|
-
| `banner` | `method_option` | Placeholder shown in help for the value slot |
|
|
212
|
-
|
|
213
|
-
---
|
|
214
|
-
|
|
215
|
-
## `_` Prefix Convention
|
|
216
|
-
|
|
217
|
-
Methods whose names start with `_` are considered gem-owned in Asgard's naming convention. `run!` guards against invoking them directly from the CLI:
|
|
218
|
-
|
|
219
|
-
```bash
|
|
220
|
-
asgard _something
|
|
221
|
-
# asgard: unknown command '_something'
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
If you define your own methods on `Tasks`, avoid the `_` prefix to prevent them from being blocked. Built-in `class_option` declarations (like `--version`, `--debug`, `--verbose`, `--doctor`) do not use the `_` prefix because they are options, not commands.
|
data/docs/schedule.md
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
# Scheduled Tasks
|
|
2
|
-
|
|
3
|
-
Asgard can run any task on a schedule. You declare schedules in the project's own `.loki`, and `asgard schedule install` hands them to the platform's own scheduler: **launchd** on macOS, **systemd user timers** on Linux. Both schedulers run a calendar job that was missed while the machine slept as soon as it wakes.
|
|
4
|
-
|
|
5
|
-
Scheduling is built into the gem, so there is nothing to import or require.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Declaring Schedules
|
|
10
|
-
|
|
11
|
-
Call `schedule` at class level inside `Tasks`:
|
|
12
|
-
|
|
13
|
-
```ruby
|
|
14
|
-
# .loki
|
|
15
|
-
class Tasks
|
|
16
|
-
desc "daily_summary", "Summarize the day's work"
|
|
17
|
-
def daily_summary = sh "bin/summary"
|
|
18
|
-
|
|
19
|
-
schedule :daily_summary, at: "17:30", on: :weekdays
|
|
20
|
-
schedule :weekly_report, at: "16:00", on: :friday
|
|
21
|
-
schedule :sync, every: 3600 # seconds
|
|
22
|
-
schedule :backup, at: %w[02:00 14:00] # on: defaults to :daily
|
|
23
|
-
|
|
24
|
-
# The task's own options go in options:, split the way a shell would
|
|
25
|
-
# (quotes respected).
|
|
26
|
-
schedule :report, options: "--format md -v", at: "08:00", on: :weekdays
|
|
27
|
-
schedule :report, options: "--period week", at: "16:00", on: :friday
|
|
28
|
-
schedule :notify, options: "--msg 'backup done'", every: 86_400, as: "notify_daily"
|
|
29
|
-
end
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
A declaration needs exactly one of `at:` or `every:`.
|
|
33
|
-
|
|
34
|
-
| Keyword | Meaning |
|
|
35
|
-
|---------|---------|
|
|
36
|
-
| `at:` | `"HH:MM"` (24-hour), or an Array of times |
|
|
37
|
-
| `on:` | `:daily` (default), `:weekdays`, `:weekends`, a day (`:friday`), or an Array of days |
|
|
38
|
-
| `every:` | Interval in seconds, or anything that responds to `in_seconds` (an ActiveSupport `Duration`) |
|
|
39
|
-
| `options:` | The task's own arguments, as a String (split shell-style) or an Array of words |
|
|
40
|
-
| `env:` | `{ "KEY" => "value" }`: literal environment variables added to the job |
|
|
41
|
-
| `as:` | The entry's name (letters, digits, `_ . -`) |
|
|
42
|
-
|
|
43
|
-
### Entry Names
|
|
44
|
-
|
|
45
|
-
Every entry has a name, and the subcommands below take it as `NAME`. The name defaults to the task name. When the entry has `options:`, the default is a slug of the task plus its options (`:report, options: "--format md -v"` becomes `report-format-md-v`), so one task can be scheduled several times with different flags. `as:` overrides the default. Declaring two *different* entries with the same name raises an error.
|
|
46
|
-
|
|
47
|
-
### Durations
|
|
48
|
-
|
|
49
|
-
Asgard doesn't depend on ActiveSupport. If you want `every: 3.minutes`, require it yourself at the top of your `.loki`:
|
|
50
|
-
|
|
51
|
-
```ruby
|
|
52
|
-
require "active_support/core_ext/integer/time"
|
|
53
|
-
|
|
54
|
-
class Tasks
|
|
55
|
-
schedule :sync, every: 15.minutes
|
|
56
|
-
end
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Managing Schedules
|
|
62
|
-
|
|
63
|
-
`schedule` is also a command with subcommands. They work the same way on both platforms:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
asgard schedule preview # print the job files install would write
|
|
67
|
-
asgard schedule install # load declared entries; drop entries no longer declared
|
|
68
|
-
asgard schedule list # this project's entries, state, last exit (--all: every project's)
|
|
69
|
-
asgard schedule stop NAME # stop one entry (stays stopped across reboots and installs)
|
|
70
|
-
asgard schedule start NAME # start a stopped entry, or install just this one
|
|
71
|
-
asgard schedule trigger NAME # run an installed entry now, under the scheduler
|
|
72
|
-
asgard schedule log NAME [-f] # print the entry's log (-f keeps following it)
|
|
73
|
-
asgard schedule remove # unload and delete all of this project's entries
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
`stop`, `start`, `trigger` and `log` also accept an entry of another project, so
|
|
77
|
-
they work from any directory. Use the name shown by `list --all`; if two projects
|
|
78
|
-
have an entry with the same name, write it as `project/name` (this project's own
|
|
79
|
-
entry wins a bare name).
|
|
80
|
-
|
|
81
|
-
Re-run `asgard schedule install` after changing declarations or your `PATH`.
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## How Jobs Run
|
|
86
|
-
|
|
87
|
-
Each entry runs `asgard <task> [options]` from the directory that holds `.loki`, so installing from any subdirectory gives the same result. The job gets the `PATH` that was current when you ran `install`, plus any `env:` variables.
|
|
88
|
-
|
|
89
|
-
If the project has a `.envrc`, the job runs under `direnv exec`, so API keys and other secrets load from `.envrc` at run time and are never copied into the job files. If `direnv` isn't on your `PATH`, `install` warns that `.envrc` will not be loaded.
|
|
90
|
-
|
|
91
|
-
Entries are scoped to the project (the name of the directory holding `.loki`), so `list`, `install` and `remove` only touch this project's jobs.
|
|
92
|
-
|
|
93
|
-
| | macOS (launchd) | Linux (systemd) |
|
|
94
|
-
|---|---|---|
|
|
95
|
-
| Job files | `~/Library/LaunchAgents/com.madbomber.asgard.<project>.<name>.plist` | `~/.config/systemd/user/asgard.<project>.<name>.{service,timer}` |
|
|
96
|
-
| Logs | `~/Library/Logs/asgard/` | `~/.local/state/asgard/` (honors `XDG_STATE_HOME`) |
|
|
97
|
-
| Stop | `launchctl disable` | `systemctl --user disable --now` |
|
|
98
|
-
| Caveats | runs only while you're logged in | runs only while you're logged in unless `loginctl enable-linger`; needs systemd 240+ |
|
|
99
|
-
|
|
100
|
-
Other platforms aren't supported. The `schedule` subcommands there exit with an error.
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## Name Collisions
|
|
105
|
-
|
|
106
|
-
The built-in command is registered as `_schedule` and mapped to `schedule`, following Asgard's `_` convention for gem-owned tasks. If your `.loki` defines its own `schedule` task, `asgard schedule` still dispatches to the built-in command.
|
data/docs/shell.md
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
# Shell Helpers
|
|
2
|
-
|
|
3
|
-
Asgard provides two methods for running shell commands and scripts from within task bodies: `sh` for shell commands and heredocs, and `shebang` for polyglot scripts. Both are provided by `Asgard::Shell` and mixed into every `Tasks` instance.
|
|
4
|
-
|
|
5
|
-
Both methods exit with the command's status code on failure — they do not raise Ruby exceptions. `sh` also accepts `exec: true` to replace the asgard process outright instead of forking — see [Handing Off with `exec`](#handing-off-with-exec).
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## `sh` — Run Shell Commands
|
|
10
|
-
|
|
11
|
-
### Single-Line Command
|
|
12
|
-
|
|
13
|
-
Pass a single-line string to run it via `system`:
|
|
14
|
-
|
|
15
|
-
```ruby
|
|
16
|
-
class Tasks
|
|
17
|
-
desc "Compile the project"
|
|
18
|
-
def build = sh "rake build"
|
|
19
|
-
|
|
20
|
-
desc "Remove build artifacts"
|
|
21
|
-
def clean = sh "rm -rf dist/ tmp/"
|
|
22
|
-
end
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
By default, `sh` prints the command before running it:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
rake build
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Multi-Line Heredoc
|
|
32
|
-
|
|
33
|
-
Pass a multiline string (e.g., a heredoc) to run it as a single `bash -c` script. All lines execute in the same shell session, so variable assignments carry across lines:
|
|
34
|
-
|
|
35
|
-
```ruby
|
|
36
|
-
class Tasks
|
|
37
|
-
desc "Bootstrap the development environment"
|
|
38
|
-
def setup
|
|
39
|
-
sh <<~SHELL
|
|
40
|
-
brew install redis postgresql
|
|
41
|
-
brew services start redis
|
|
42
|
-
bundle install
|
|
43
|
-
rails db:setup
|
|
44
|
-
SHELL
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Asgard detects the newline and automatically routes multiline scripts through `bash -c`.
|
|
50
|
-
|
|
51
|
-
### Silent Mode
|
|
52
|
-
|
|
53
|
-
Pass `silent: true` to suppress the command echo. The command still runs and still exits on failure; it just doesn't print the command text first:
|
|
54
|
-
|
|
55
|
-
```ruby
|
|
56
|
-
class Tasks
|
|
57
|
-
desc "Compile (quiet)"
|
|
58
|
-
def build = sh "rake build", silent: true
|
|
59
|
-
|
|
60
|
-
desc "Print environment info without noise"
|
|
61
|
-
def info
|
|
62
|
-
sh "printenv | grep APP_", silent: true
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Exit on Failure
|
|
68
|
-
|
|
69
|
-
`sh` always calls `exit($?.exitstatus)` if the command fails. There is no rescue path — a failing command terminates the `asgard` process. This is intentional: failed steps should stop the pipeline rather than silently continue.
|
|
70
|
-
|
|
71
|
-
```ruby
|
|
72
|
-
class Tasks
|
|
73
|
-
depends_on :test
|
|
74
|
-
desc "Test then release"
|
|
75
|
-
def release
|
|
76
|
-
sh "bundle exec rake release"
|
|
77
|
-
# Never reached if rake release fails
|
|
78
|
-
puts "Released!"
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Handing Off with `exec`
|
|
84
|
-
|
|
85
|
-
Pass `exec: true` to hand the command the asgard process itself instead of forking a child. Under the hood this calls `Kernel.exec` rather than `system`, which replaces the running ruby process image with the command — asgard doesn't stick around waiting on it.
|
|
86
|
-
|
|
87
|
-
Use this for a task's final, long-running command — a dev server, a REPL, anything meant to take over the terminal — so there's no idle ruby process sitting in memory alongside it, and Ctrl-C is handled directly by the command instead of unwinding back through asgard:
|
|
88
|
-
|
|
89
|
-
```ruby
|
|
90
|
-
class Tasks
|
|
91
|
-
desc "Documentation server startup"
|
|
92
|
-
depends_on :doc_builder
|
|
93
|
-
def doc_server = sh "mkdocs serve", exec: true
|
|
94
|
-
end
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Since the process is replaced, nothing after the `sh` call ever runs, and `depends_on` chains upstream of it must already have finished (they have — dependencies run before the task body).
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
class Tasks
|
|
101
|
-
desc "Start a REPL — never returns to asgard"
|
|
102
|
-
def console
|
|
103
|
-
sh "bundle exec pry", exec: true
|
|
104
|
-
puts "unreachable"
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
!!! note
|
|
110
|
-
`exec: true` only makes sense for a command meant to run for the lifetime of the process. Don't use it for a step with more work queued after it in the same task.
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## `shebang` — Polyglot Scripts
|
|
115
|
-
|
|
116
|
-
`shebang` writes the script body to a tempfile with the appropriate extension and executes it with the specified interpreter. Use it to embed Python, Node.js, Ruby, Perl, or any other interpreter directly in a task:
|
|
117
|
-
|
|
118
|
-
### Python
|
|
119
|
-
|
|
120
|
-
```ruby
|
|
121
|
-
class Tasks
|
|
122
|
-
desc "Run Python data analysis"
|
|
123
|
-
def analyze
|
|
124
|
-
shebang :python3, <<~PYTHON
|
|
125
|
-
import json
|
|
126
|
-
data = json.load(open("results.json"))
|
|
127
|
-
print(f"Total: {sum(data.values())}")
|
|
128
|
-
PYTHON
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Node.js
|
|
134
|
-
|
|
135
|
-
```ruby
|
|
136
|
-
class Tasks
|
|
137
|
-
desc "Build frontend assets with esbuild"
|
|
138
|
-
def bundle_assets
|
|
139
|
-
shebang :node, <<~JS
|
|
140
|
-
const esbuild = require("esbuild")
|
|
141
|
-
esbuild.buildSync({
|
|
142
|
-
entryPoints: ["src/app.js"],
|
|
143
|
-
bundle: true,
|
|
144
|
-
outfile: "dist/app.js"
|
|
145
|
-
})
|
|
146
|
-
JS
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Ruby
|
|
152
|
-
|
|
153
|
-
```ruby
|
|
154
|
-
class Tasks
|
|
155
|
-
desc "Transform data with Ruby"
|
|
156
|
-
def transform
|
|
157
|
-
shebang :ruby, <<~RUBY
|
|
158
|
-
require "json"
|
|
159
|
-
data = JSON.parse(File.read("input.json"))
|
|
160
|
-
File.write("output.json", JSON.pretty_generate(data.transform_values(&:upcase)))
|
|
161
|
-
RUBY
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Bash
|
|
167
|
-
|
|
168
|
-
```ruby
|
|
169
|
-
class Tasks
|
|
170
|
-
desc "Run a bash provisioning script"
|
|
171
|
-
def provision
|
|
172
|
-
shebang :bash, <<~BASH
|
|
173
|
-
set -euo pipefail
|
|
174
|
-
apt-get update
|
|
175
|
-
apt-get install -y curl wget git
|
|
176
|
-
echo "Provisioned at $(date)"
|
|
177
|
-
BASH
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
## Supported Interpreters
|
|
185
|
-
|
|
186
|
-
| Symbol | File Extension | Interpreter |
|
|
187
|
-
|---|---|---|
|
|
188
|
-
| `:python3` | `.py` | `python3` |
|
|
189
|
-
| `:python` | `.py` | `python` |
|
|
190
|
-
| `:node` | `.js` | `node` |
|
|
191
|
-
| `:ruby` | `.rb` | `ruby` |
|
|
192
|
-
| `:perl` | `.pl` | `perl` |
|
|
193
|
-
| `:bash` | `.sh` | `bash` |
|
|
194
|
-
| `:sh` | `.sh` | `sh` |
|
|
195
|
-
| Any other symbol | `.tmp` | Passed directly to `system` |
|
|
196
|
-
|
|
197
|
-
!!! note
|
|
198
|
-
Unknown interpreter symbols get a `.tmp` extension and are passed to `system` directly. This makes it easy to use interpreters not in the table above:
|
|
199
|
-
|
|
200
|
-
```ruby
|
|
201
|
-
shebang :lua, <<~LUA
|
|
202
|
-
print("Hello from Lua!")
|
|
203
|
-
LUA
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### Silent Mode
|
|
207
|
-
|
|
208
|
-
`shebang` also accepts `silent: true`, though in practice the interpreter itself controls what is printed:
|
|
209
|
-
|
|
210
|
-
```ruby
|
|
211
|
-
def analyze
|
|
212
|
-
shebang :python3, script_body, silent: true
|
|
213
|
-
end
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
---
|
|
217
|
-
|
|
218
|
-
## Combining `sh` and `shebang`
|
|
219
|
-
|
|
220
|
-
You can mix both in the same task:
|
|
221
|
-
|
|
222
|
-
```ruby
|
|
223
|
-
class Tasks
|
|
224
|
-
desc "Run a mixed shell + Python pipeline"
|
|
225
|
-
def pipeline
|
|
226
|
-
sh "bundle exec rake build"
|
|
227
|
-
|
|
228
|
-
shebang :python3, <<~PYTHON
|
|
229
|
-
import subprocess
|
|
230
|
-
result = subprocess.run(["./bin/validate"], capture_output=True, text=True)
|
|
231
|
-
print(result.stdout)
|
|
232
|
-
PYTHON
|
|
233
|
-
|
|
234
|
-
sh "rake deploy"
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
```
|