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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +179 -99
  3. data/README.md +141 -46
  4. data/bin/asgard +10 -0
  5. data/lib/asgard/base/dispatch.rb +257 -31
  6. data/lib/asgard/base/registry.rb +7 -3
  7. data/lib/asgard/base.rb +140 -9
  8. data/lib/asgard/declarations.rb +21 -0
  9. data/lib/asgard/doctor/task_sections.rb +11 -3
  10. data/lib/asgard/doctor.rb +12 -8
  11. data/lib/asgard/jobs/commands.rb +152 -0
  12. data/lib/asgard/jobs/declaration.rb +149 -0
  13. data/lib/asgard/jobs/launcher.rb +109 -0
  14. data/lib/asgard/jobs/state.rb +190 -0
  15. data/lib/asgard/jobs/stopper.rb +141 -0
  16. data/lib/asgard/jobs.rb +25 -0
  17. data/lib/asgard/kernel_methods.rb +19 -14
  18. data/lib/asgard/leading_flag.rb +10 -0
  19. data/lib/asgard/pid.rb +130 -0
  20. data/lib/asgard/schedule/commands.rb +39 -11
  21. data/lib/asgard/schedule/declaration.rb +139 -12
  22. data/lib/asgard/schedule/job_files.rb +24 -0
  23. data/lib/asgard/schedule/launchd.rb +55 -18
  24. data/lib/asgard/schedule/systemd.rb +69 -27
  25. data/lib/asgard/schedule/table.rb +34 -16
  26. data/lib/asgard/schedule.rb +49 -9
  27. data/lib/asgard/shell.rb +372 -44
  28. data/lib/asgard/state_dir.rb +84 -0
  29. data/lib/asgard/tasks.rb +8 -0
  30. data/lib/asgard/version.rb +1 -1
  31. data/lib/asgard/watch/changes.rb +26 -1
  32. data/lib/asgard/watch/commands.rb +52 -12
  33. data/lib/asgard/watch/daemon.rb +334 -43
  34. data/lib/asgard/watch/declaration.rb +135 -40
  35. data/lib/asgard/watch/dispatcher.rb +61 -10
  36. data/lib/asgard/watch/mailbox.rb +50 -0
  37. data/lib/asgard/watch/reporter.rb +50 -34
  38. data/lib/asgard/watch/spawn.rb +41 -16
  39. data/lib/asgard/watch/state.rb +93 -27
  40. data/lib/asgard/watch/worker.rb +44 -9
  41. data/lib/asgard/watch.rb +4 -11
  42. data/lib/asgard.rb +80 -14
  43. metadata +167 -186
  44. data/.envrc +0 -6
  45. data/.github/workflows/deploy-github-pages.yml +0 -52
  46. data/.loki +0 -39
  47. data/.reek.yml +0 -166
  48. data/.rubocop.yml +0 -163
  49. data/Archspec.rb +0 -24
  50. data/CLAUDE.md +0 -139
  51. data/COMMITS.md +0 -196
  52. data/bin/console +0 -11
  53. data/bin/setup +0 -8
  54. data/doc_tasks.loki +0 -21
  55. data/docs/api.md +0 -368
  56. data/docs/assets/css/custom.css +0 -93
  57. data/docs/assets/images/asgard.jpg +0 -0
  58. data/docs/changelog.md +0 -1
  59. data/docs/dependencies.md +0 -313
  60. data/docs/environment.md +0 -129
  61. data/docs/examples.md +0 -194
  62. data/docs/getting-started.md +0 -181
  63. data/docs/helpers.md +0 -262
  64. data/docs/index.md +0 -87
  65. data/docs/options.md +0 -224
  66. data/docs/schedule.md +0 -106
  67. data/docs/shell.md +0 -237
  68. data/docs/subcommands.md +0 -181
  69. data/docs/task-files.md +0 -407
  70. data/docs/tasks.md +0 -326
  71. data/docs/variables.md +0 -338
  72. data/docs/watch.md +0 -123
  73. data/examples/.env +0 -4
  74. data/examples/.envrc +0 -3
  75. data/examples/.loki +0 -24
  76. data/examples/bad.loki +0 -63
  77. data/examples/concurrent.loki +0 -58
  78. data/examples/confirm.loki +0 -48
  79. data/examples/db_subcommands.loki +0 -74
  80. data/examples/depends_on_block/bad/.loki +0 -51
  81. data/examples/depends_on_block/good/.loki +0 -83
  82. data/examples/do.loki +0 -45
  83. data/examples/env_usage.loki +0 -27
  84. data/examples/kitchen_sink.loki +0 -210
  85. data/examples/server_subcommands.loki +0 -65
  86. data/examples/subdir/.loki +0 -12
  87. data/examples/subdir/import_demo.loki +0 -14
  88. data/examples/subdir/import_up_demo.loki +0 -18
  89. data/examples/watcher_examples/.envrc +0 -3
  90. data/examples/watcher_examples/.loki +0 -54
  91. data/examples/watcher_examples/README.md +0 -112
  92. data/examples/watcher_examples/bin/hello +0 -3
  93. data/examples/watcher_examples/data/records.json +0 -1
  94. data/examples/watcher_examples/docs/drafts/ideas.md +0 -5
  95. data/examples/watcher_examples/docs/index.md +0 -4
  96. data/examples/watcher_examples/lib/greeter.rb +0 -7
  97. data/gem_tasks.loki +0 -51
  98. data/git.loki +0 -13
  99. data/mkdocs.yml +0 -166
  100. data/quality.loki +0 -266
  101. data/quality_rails.loki +0 -47
  102. data/xyzzy.loki +0 -12
data/docs/dependencies.md DELETED
@@ -1,313 +0,0 @@
1
- # Task Dependencies
2
-
3
- `depends_on` declares what must run before a task. Asgard resolves the dependency graph at startup, validates it for cycles, and executes prerequisites automatically when a task is invoked.
4
-
5
- !!! note
6
- `desc` and `depends_on` are independent — either can come first. Both must appear before the `def`.
7
-
8
- ---
9
-
10
- ## How It Works
11
-
12
- When you run `asgard <task>`, Asgard:
13
-
14
- 1. Validates the full dependency graph for circular references (fails fast with a clear error).
15
- 2. Resolves the dependency stages for the requested task in order.
16
- 3. Executes each stage — running parallel groups in native Ruby threads.
17
- 4. Runs the task itself after all prerequisites complete.
18
-
19
- **Deduplication:** each task runs at most once per `asgard` invocation, regardless of how many other tasks declare it as a dependency. This is enforced thread-safely via a class-level `Set` and `Mutex`.
20
-
21
- ---
22
-
23
- ## Sequential Dependencies
24
-
25
- Bare symbols run one after another in the order declared:
26
-
27
- ```ruby
28
- class Tasks
29
- desc "Compile the project"
30
- def build = sh "rake build"
31
-
32
- depends_on :build
33
- desc "Run the test suite"
34
- def test = sh "rake test"
35
-
36
- depends_on :test
37
- desc "Publish the gem"
38
- def release = sh "bundle exec rake release"
39
- end
40
- ```
41
-
42
- ```bash
43
- asgard release # build → test → release
44
- ```
45
-
46
- Multiple sequential dependencies in a single `depends_on` call run left to right:
47
-
48
- ```ruby
49
- depends_on :clean, :build, :test
50
- desc "Clean, build, and test"
51
- def package = sh "rake package"
52
- ```
53
-
54
- ---
55
-
56
- ## Parallel Dependencies
57
-
58
- 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:
59
-
60
- ```ruby
61
- class Tasks
62
- desc "Check code style"
63
- def lint = sh "bundle exec rubocop"
64
-
65
- desc "Run type checks"
66
- def typecheck = sh "bundle exec srb tc"
67
-
68
- depends_on [:lint, :typecheck]
69
- desc "Run tests (after lint and typecheck)"
70
- def test = sh "bundle exec rake test"
71
- end
72
- ```
73
-
74
- ```bash
75
- asgard test # lint ∥ typecheck → test
76
- ```
77
-
78
- Parallel groups run in native Ruby threads. For CPU-bound work, keep in mind the GVL; for I/O-bound work (shell commands, network), true concurrency is achieved.
79
-
80
- ---
81
-
82
- ## Mixed Sequential and Parallel
83
-
84
- Mix bare symbols and arrays in a single `depends_on` call. Execution proceeds stage by stage — each stage completes before the next begins:
85
-
86
- ```ruby
87
- class Tasks
88
- desc "Install dependencies"; def setup = sh "bundle install"
89
- desc "Check code style"; def lint = sh "bundle exec rubocop"
90
- desc "Compile assets"; def build = sh "rake assets:precompile"
91
- desc "Run tests"; def test = sh "bundle exec rake test"
92
- desc "Post to Slack"; def notify = sh "curl $SLACK_WEBHOOK -d '{\"text\":\"done\"}'"
93
-
94
- # setup first, then lint+build in parallel, then test, then notify
95
- depends_on :setup, [:lint, :build], :test, :notify
96
- desc "Full CI pipeline"
97
- def ci = puts "CI complete"
98
- end
99
- ```
100
-
101
- ```bash
102
- asgard ci
103
- ```
104
-
105
- Execution order:
106
-
107
- ```
108
- setup
109
- ↓
110
- lint ∥ build (concurrent)
111
- ↓
112
- test
113
- ↓
114
- notify
115
- ↓
116
- ci
117
- ```
118
-
119
- ---
120
-
121
- ## Deduplication
122
-
123
- Each task runs at most once per `asgard` invocation. If multiple tasks declare the same dependency, it executes only on its first encounter:
124
-
125
- ```ruby
126
- class Tasks
127
- desc "Install gems"
128
- def setup = sh "bundle install"
129
-
130
- depends_on :setup
131
- desc "Run tests"
132
- def test = sh "rake test"
133
-
134
- depends_on :setup
135
- desc "Check style"
136
- def lint = sh "rubocop"
137
-
138
- depends_on [:test, :lint]
139
- desc "Test and lint (setup runs once)"
140
- def ci = puts "done"
141
- end
142
- ```
143
-
144
- When `asgard ci` runs, `setup` executes once even though both `test` and `lint` declare it as a dependency. The deduplication set is managed with a `Mutex` so parallel groups are also safe.
145
-
146
- ---
147
-
148
- ## Transitive Dependencies
149
-
150
- When a dependency has its own dependencies, Asgard resolves them recursively before running the dependent task. The deduplication set ensures each task runs at most once regardless of how many paths lead to it.
151
-
152
- Consider this graph:
153
-
154
- ```ruby
155
- class Tasks
156
- desc "Fetch gems"
157
- def setup = sh "bundle install"
158
-
159
- depends_on :setup
160
- desc "Compile assets"
161
- def build = sh "rake assets:precompile"
162
-
163
- desc "Check code style"
164
- def lint = sh "bundle exec rubocop"
165
-
166
- depends_on :build, :lint, :setup
167
- desc "Run the full pipeline"
168
- def ci = puts "Done."
169
- end
170
- ```
171
-
172
- `ci` declares three sequential dependencies: `build`, `lint`, `setup`. But `build` itself depends on `setup`. The effective execution order is:
173
-
174
- ```
175
- setup ← run as build's prerequisite
176
- ↓
177
- build
178
- ↓
179
- lint
180
- ↓
181
- (setup skipped — already done)
182
- ↓
183
- ci
184
- ```
185
-
186
- `setup` runs once — on its first encounter as `build`'s prerequisite. When `ci`'s own stage for `setup` is reached, the deduplication set skips it.
187
-
188
- !!! tip
189
- When a task is both a transitive dependency and a direct dependency, declare it only where it logically belongs — as a prerequisite of the task that needs it. Declaring it redundantly at the top level is harmless (deduplication handles it) but adds noise.
190
-
191
- ---
192
-
193
- ## Circular Dependency Detection
194
-
195
- Asgard validates the full dependency graph using stdlib [TSort](https://docs.ruby-lang.org/en/master/TSort.html) before any task runs. A circular dependency produces a clean error and exits:
196
-
197
- ```ruby
198
- class Tasks
199
- depends_on :b
200
- desc "Task A"; def a = puts "a"
201
-
202
- depends_on :a
203
- desc "Task B"; def b = puts "b"
204
- end
205
- ```
206
-
207
- ```bash
208
- asgard a
209
- # asgard: circular dependency — TSort::Cyclic: ...
210
- ```
211
-
212
- No backtrace is shown — just a single diagnostic line.
213
-
214
- ---
215
-
216
- ## depends_on Across Multiple Files
217
-
218
- `depends_on` works across `.loki` files because all files reopen the same `class Tasks`. The dependency is recorded when the `def` is encountered, so load order matters:
219
-
220
- ```ruby
221
- # build.loki
222
- class Tasks
223
- desc "Compile"
224
- def build = sh "rake build"
225
- end
226
-
227
- # test.loki
228
- class Tasks
229
- depends_on :build # build.loki must be loaded first
230
- desc "Test"
231
- def test = sh "rake test"
232
- end
233
- ```
234
-
235
- Because `*.loki` files are loaded alphabetically when `import "*.loki"` is used, `build.loki` loads before `test.loki`. If you need to control load order precisely, use explicit `import` calls with full filenames rather than a glob.
236
-
237
- ---
238
-
239
- ## Dynamic Dependencies (Proc / Block Form)
240
-
241
- `depends_on` normally takes a fixed list, recorded the moment the `def` right after it is encountered — which is why load order matters, as above. Pass a `Proc` or lambda instead — or, equivalently, a block — and that list is computed *later*, after every `.loki` file has finished loading, rather than at the point `depends_on` itself is evaluated:
242
-
243
- ```ruby
244
- depends_on -> { [all_commands.keys.grep(/_check\z/).sort.map(&:to_sym)] }
245
- desc "Run every *_check quality gate task in parallel"
246
- def quality
247
- # ...
248
- end
249
-
250
- depends_on { [all_commands.keys.grep(/_check\z/).sort.map(&:to_sym)] }
251
- desc "Same thing, written as a block"
252
- def quality2
253
- # ...
254
- end
255
- ```
256
-
257
- This solves exactly the "load order matters" problem from the previous section: a plain array can only name tasks that already exist in `.loki` files loaded *before* this one. A Proc/block is resolved once every file has loaded, so it can safely reference a task defined in a file that hasn't been imported yet at the point `depends_on` is written — including one that only exists conditionally, e.g. a Rails-specific task file imported with `import "quality_rails.loki" if defined?(Rails)`. `depends_on` accepts task arguments *or* a block, never both — combining them raises `Asgard::Error`.
258
-
259
- **Shape:** the Proc/block must return exactly what the plain-array form would receive as its splat arguments — an array of stages, each a `Symbol`/`String` (sequential) or an `Array` of `Symbol`/`String` (parallel group), nested no deeper than that. The example above returns `[[:a_check, :b_check, :c_check]]`: one stage, containing every matching task, all running in parallel — the same shape as `depends_on [:a_check, :b_check, :c_check]`. This shape is validated once the result comes back — a bad return value (wrong type, a stage that isn't a Symbol/String/Array, a leaf inside a parallel group that isn't a Symbol/String, or nesting more than one level deep) raises `Asgard::Error` naming the task and the offending value, instead of failing later with an opaque `NoMethodError`:
260
-
261
- ```bash
262
- asgard quality
263
- # asgard: depends_on proc/block for 'quality' returned invalid stage 123 (Integer); expected a Symbol, String, or Array of them
264
- ```
265
-
266
- **When it runs:** once, when `validate_deps!` runs (right after the `.loki` chain finishes loading, before any task dispatches). The resolved result replaces the Proc/block in the dependency table, so cycle detection, undefined-task checks, and arity checks all run against the *resolved* list — a Proc/block that references an undefined task, or that itself introduces a cycle, is caught at startup exactly like a plain array would be:
267
-
268
- ```bash
269
- asgard quality
270
- # asgard: undefined task(s) in depends_on: ghost_check
271
- ```
272
-
273
- **`self` inside the Proc/block:** since it's written directly in a `class Tasks` body, it lexically captures that class as `self` — so it can call `all_commands`, `_deps`, or any other class-level method bare, without a `self.class.` prefix, even though it's actually invoked later from inside `validate_deps!`.
274
-
275
- **If the Proc/block raises**, the error is caught and re-raised as `Asgard::Error` naming the task it was declared for:
276
-
277
- ```
278
- asgard: depends_on proc for 'quality' raised RuntimeError: boom
279
- ```
280
-
281
- !!! tip
282
- Reach for this only when the dependency list genuinely can't be known until every file has loaded — like "every task whose name ends in `_check`," discovered across several `.loki` files. For a fixed, known-upfront list, the plain array form is simpler and reads just as clearly.
283
-
284
- ---
285
-
286
- ## depends_on Inside Subcommands
287
-
288
- `depends_on` works within subcommand classes exactly as it does at the top level. Dependency scope is per-class:
289
-
290
- ```ruby
291
- class DBCommands < Tasks
292
- desc "Run migrations"
293
- def migrate = sh "rails db:migrate"
294
-
295
- desc "Load seed data"
296
- def seed = sh "rails db:seed"
297
-
298
- depends_on :migrate, :seed
299
- desc "Migrate then seed"
300
- def reset = puts "Done."
301
- end
302
-
303
- class Tasks
304
- desc "db SUBCOMMAND", "Manage the database"
305
- subcommand "db", DBCommands
306
- end
307
- ```
308
-
309
- ```bash
310
- asgard db reset # migrate → seed → reset
311
- ```
312
-
313
- See [Subcommands](subcommands.md) for the full guide.
data/docs/environment.md DELETED
@@ -1,129 +0,0 @@
1
- # Environment Variables
2
-
3
- Asgard provides the `dotenv` class method to load `.env` files into the process environment before tasks run. It is a thin wrapper around the [dotenv gem](https://github.com/bkeepers/dotenv).
4
-
5
- ---
6
-
7
- ## Basic Usage
8
-
9
- Call `dotenv` inside the class body (not inside a task method) to load the default `.env` file:
10
-
11
- ```ruby
12
- class Tasks
13
- dotenv # loads .env from the current working directory
14
-
15
- desc "Print the app name from .env"
16
- def check = puts env(:app_name)
17
- end
18
- ```
19
-
20
- ### Load a Named File
21
-
22
- Pass a file path string to load a specific file:
23
-
24
- ```ruby
25
- class Tasks
26
- dotenv ".env.local" # load a local override
27
- dotenv ".env.staging" # load staging-specific vars
28
- end
29
- ```
30
-
31
- ### Multiple Calls
32
-
33
- Call `dotenv` multiple times to load several files. Each call merges the loaded variables into `ENV`. Later calls do not overwrite variables already set by earlier calls (standard dotenv behavior):
34
-
35
- ```ruby
36
- class Tasks
37
- dotenv # loads .env (base config)
38
- dotenv ".env.local" # loads .env.local (local overrides)
39
- end
40
- ```
41
-
42
- ---
43
-
44
- ## When `dotenv` Runs
45
-
46
- `dotenv` is a **class-level** call — it executes at Ruby class-load time, not when a task is invoked. This means:
47
-
48
- 1. Variables are available in `ENV` before any task method runs.
49
- 2. They are available during `depends_on` dependency resolution.
50
-
51
- ```ruby
52
- class Tasks
53
- dotenv
54
-
55
- desc "Run migrations"
56
- def migrate = sh "DATABASE_URL=#{env(:database_url)} rails db:migrate"
57
- end
58
- ```
59
-
60
- !!! warning
61
- If `.env` does not exist, `dotenv` silently does nothing — it checks `File.exist?` before loading. There is no error for a missing file.
62
-
63
- ---
64
-
65
- ## File Not Found
66
-
67
- Asgard calls `Dotenv.load(path)` only when `File.exist?(path)` is true. If the file is absent, the call is a no-op:
68
-
69
- ```ruby
70
- class Tasks
71
- dotenv ".env.local" # silently skipped if .env.local does not exist
72
- end
73
- ```
74
-
75
- This makes it safe to commit a `.env.local` line to your `.loki` without requiring every developer to create the file.
76
-
77
- ---
78
-
79
- ## Environment Variables vs. Class Variables
80
-
81
- Use `dotenv` to bring external configuration into `ENV`. Use `@@` class variables for fixed values declared in the task file, and read from `ENV` directly in task bodies or helper methods when the value comes from the environment:
82
-
83
- ```ruby
84
- class Tasks
85
- dotenv
86
-
87
- @@app_name ||= "myapp".freeze
88
-
89
- desc "Start the server"
90
- def start
91
- port = ENV.fetch("PORT", "3000").to_i
92
- sh "puma -b tcp://0.0.0.0:#{port} -w #{ENV.fetch('WORKERS', '2')}"
93
- end
94
- end
95
- ```
96
-
97
- For `ENV` values used in multiple tasks, define a private helper method:
98
-
99
- ```ruby
100
- class Tasks
101
- dotenv
102
-
103
- desc "Start the server"
104
- def start = sh "puma -p #{port}"
105
-
106
- desc "Show config"
107
- def config = puts "#{@@app_name} on port #{port}"
108
-
109
- private
110
-
111
- def port = ENV.fetch("PORT", "3000").to_i
112
- end
113
- ```
114
-
115
- ---
116
-
117
- ## Dotenv File Format
118
-
119
- Standard dotenv file syntax applies:
120
-
121
- ```bash
122
- # .env
123
- APP_NAME=myapp
124
- DATABASE_URL=postgres://localhost/myapp_development
125
- REDIS_URL=redis://localhost:6379/0
126
- PORT=3000
127
- ```
128
-
129
- Multi-line values and quotes are supported per the dotenv gem's own documentation.
data/docs/examples.md DELETED
@@ -1,194 +0,0 @@
1
- # Examples
2
-
3
- The `examples/` directory in the Asgard repository contains complete, working `.loki` files that demonstrate every feature of the gem. You can use them as a standalone Asgard project or as copy-paste references.
4
-
5
- ---
6
-
7
- ## Using the Examples Directory
8
-
9
- The `examples/` directory contains its own `.loki` root marker (the gem's project-level `.loki`), which means you can run `asgard` from inside the `examples/` directory and all example files will be loaded:
10
-
11
- ```bash
12
- git clone https://github.com/MadBomber/asgard.git
13
- cd asgard/examples
14
- asgard help
15
- ```
16
-
17
- Alternatively, copy individual example files into your own project's directory.
18
-
19
- !!! note
20
- Some examples (notably `concurrent.loki`) produce visible interleaved output to demonstrate real thread concurrency. They are designed to be run, not just read.
21
-
22
- ---
23
-
24
- ## `kitchen_sink.loki`
25
-
26
- **Path:** `examples/kitchen_sink.loki`
27
-
28
- The most comprehensive example — demonstrates every Thor DSL feature available in Asgard:
29
-
30
- - `@@` class variables for shared configuration values
31
- - `dotenv` (commented out, ready to activate)
32
- - `class_option` with `:boolean` and `:string` types, including `enum`
33
- - `default_task` — sets the default command when `asgard` is run with no arguments
34
- - `map` — short aliases for multiple tasks
35
- - A basic task with no parameters
36
- - A task with a positional parameter and default
37
- - A task with `option` (the `method_option` alias)
38
- - All five `method_option` types: `:string`, `:boolean`, `:numeric`, `:array`, `:hash`
39
- - `required` option, `enum` validation, and `banner` customization
40
- - `long_desc` with `\x5` line-break trick for formatted examples in help text
41
- - Sequential `depends_on` (`:analyze` before `:spec`)
42
- - Parallel `depends_on` (`[:analyze, :typecheck]` run concurrently)
43
- - Mixed sequential + parallel `depends_on` (`:check`, `[:compile, :spec]`, `:pack`)
44
- - `no_commands` block for a public helper excluded from CLI
45
- - `private` methods for internal helpers
46
-
47
- ```bash
48
- asgard help # see all tasks
49
- asgard greet # default task
50
- asgard hello Alice
51
- asgard compile --jobs 4 --tags debug release --defines VERSION:2 MODE:fast
52
- asgard deploy --strategy rolling
53
- asgard report --format html --since 2024-01-01
54
- asgard pipeline
55
- ```
56
-
57
- ---
58
-
59
- ## Server Subcommands
60
-
61
- **Path:** `examples/server_subcommands.loki`
62
-
63
- Demonstrates Thor subcommands with a server management group. Covers:
64
-
65
- - Defining a subcommand class (`ServerCommands < Tasks`)
66
- - Registering it with `subcommand "server", ServerCommands`
67
- - Per-command options (`--daemon`, `--workers`, `--log`, `--force`, `--wait`)
68
- - `depends_on` inside a subcommand group (`:stop` and `:start` before `:restart`)
69
-
70
- ```bash
71
- asgard server help
72
- asgard server start
73
- asgard server start 4000 --workers 4 --daemon
74
- asgard server stop --force
75
- asgard server status
76
- asgard server restart 4000
77
- ```
78
-
79
- The `ServerCommands` class inherits from `Tasks`, giving it access to `sh`, `depends_on`, and the built-in `--debug`/`--verbose` flags.
80
-
81
- ---
82
-
83
- ## DB Subcommands
84
-
85
- **Path:** `examples/db_subcommands.loki`
86
-
87
- Demonstrates subcommands with more complex `depends_on` chaining within the group. Covers:
88
-
89
- - `DBCommands < Tasks` with migrate, rollback, seed, reset, console, and status commands
90
- - Multi-step `depends_on` chain: `rollback → migrate → seed → reset`
91
- - `long_desc` with formatted examples inside a subcommand class
92
- - `enum` validation on subcommand options
93
- - Optional positional parameters (`migrate [VERSION]`, `rollback [STEPS]`, `seed [FILE]`)
94
-
95
- ```bash
96
- asgard db help
97
- asgard db migrate
98
- asgard db migrate 20240101120000 --dry-run
99
- asgard db rollback
100
- asgard db rollback 3
101
- asgard db seed --env staging
102
- asgard db reset # rollback → migrate → seed → reset
103
- asgard db console --env staging
104
- asgard db status
105
- ```
106
-
107
- ---
108
-
109
- ## `concurrent.loki`
110
-
111
- **Path:** `examples/concurrent.loki`
112
-
113
- A focused demonstration of true concurrent execution via parallel `depends_on` groups:
114
-
115
- - Three worker tasks (`worker_a`, `worker_b`, `worker_c`) each print a character repeatedly with random sleep delays
116
- - All three run in parallel threads when `asgard finish` is invoked
117
- - The interleaved output proves that real concurrency is occurring (not sequential batching)
118
- - Uses `$stdout.sync = true` to ensure thread-safe immediate output flushing
119
-
120
- ```bash
121
- asgard finish
122
- # starting demo of concurrent task execution ...
123
- # ABCBACBACBABCBACBACB (order varies every run)
124
- # fini - the end of concurrent task demo
125
- ```
126
-
127
- Execution order: `start` → `worker_a ∥ worker_b ∥ worker_c` → `finish`
128
-
129
- This example is also useful as a test harness for verifying that parallel execution is working correctly on a given system.
130
-
131
- ---
132
-
133
- ## `do.loki`
134
-
135
- **Path:** `examples/do.loki`
136
-
137
- A plain user task that runs other tasks in sequence, the way `rake a b c` or `just a b c` does. No gem changes needed:
138
-
139
- - Steps are comma-separated, so each step can carry its own arguments and options; with no commas, every word is a task of its own
140
- - Each step is parsed by Thor as if it were typed on the command line
141
- - The sequence stops at the first failure (a failing `sh`, or a step returning `:fail`)
142
-
143
- ```bash
144
- asgard do greet finish
145
- asgard do hello Alice, farewell Bob --formal
146
- # Hello, Alice!
147
- # Goodbye, Bob.
148
- ```
149
-
150
- !!! note
151
- Every task runs at most once per `asgard` invocation, so `asgard do hello Alice, hello Bob` greets only Alice. The same rule makes a dependency shared by several steps run once.
152
-
153
- ---
154
-
155
- ## `confirm.loki`
156
-
157
- **Path:** `examples/confirm.loki`
158
-
159
- A guard task. List `confirm` first in a dangerous task's `depends_on`, and the task runs only after the user answers yes:
160
-
161
- ```ruby
162
- depends_on :confirm
163
- desc "Drop and recreate the database"
164
- def reset_database = sh "bin/rails db:reset"
165
- ```
166
-
167
- ```bash
168
- asgard reset_database
169
- # Really run `asgard reset_database`? [y/N] n
170
- # asgard: aborted `asgard reset_database` (exit 1)
171
-
172
- ASGARD_YES=1 asgard reset_database # no question, for scripts and CI
173
- ```
174
-
175
- - Answering no **raises** `Asgard::Error`, which stops the dependent task. Returning `:fail` would not: a dependency's return value never stops the task that depends on it
176
- - A dependency doesn't know which task asked for it, so the question shows the whole command line
177
- - With no terminal (a schedule, the watcher, a pipe) it refuses unless `ASGARD_YES` is set
178
- - `confirm` is declared with `hide: true`, so it stays out of `asgard help`
179
-
180
- !!! note
181
- `confirm` runs at most once per `asgard` invocation, so `asgard do reset_database, drop_cache` asks once for the whole line. Keep it in its own first stage: in `depends_on [:confirm, :backup]`, `backup` runs while the question waits.
182
-
183
- ---
184
-
185
- ## Summary
186
-
187
- | File | Primary Focus |
188
- |---|---|
189
- | `kitchen_sink.loki` | Comprehensive Thor DSL reference — options, aliases, long_desc, depends_on |
190
- | `server_subcommands.loki` | Subcommand groups, per-command options, depends_on in subcommands |
191
- | `db_subcommands.loki` | Multi-step depends_on chains, enum validation, long_desc in subcommands |
192
- | `concurrent.loki` | Parallel task execution, thread concurrency demonstration |
193
- | `do.loki` | Running several tasks in sequence from one command line |
194
- | `confirm.loki` | A guard dependency that asks before a dangerous task runs |