asgard 0.3.0 → 0.3.2
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/.envrc +5 -0
- data/.loki +19 -23
- data/.reek.yml +112 -0
- data/.rubocop.yml +6 -0
- data/Archspec.rb +24 -0
- data/CHANGELOG.md +98 -2
- data/CLAUDE.md +4 -7
- data/README.md +93 -2
- data/bin/asgard +1 -1
- data/docs/api.md +130 -10
- data/docs/changelog.md +28 -0
- data/docs/dependencies.md +83 -2
- data/docs/getting-started.md +4 -2
- data/docs/helpers.md +84 -0
- data/docs/index.md +3 -3
- data/docs/options.md +51 -7
- data/docs/task-files.md +2 -2
- data/examples/bad.loki +63 -0
- data/examples/server_subcommands.loki +31 -22
- data/gem_tasks.loki +51 -0
- data/git.loki +13 -0
- data/lib/asgard/base/dependency_graph.rb +124 -0
- data/lib/asgard/base/dispatch.rb +160 -0
- data/lib/asgard/base/registry.rb +38 -0
- data/lib/asgard/base/task_dsl.rb +65 -0
- data/lib/asgard/base.rb +44 -222
- data/lib/asgard/doctor/report.rb +73 -0
- data/lib/asgard/doctor/task_sections.rb +67 -0
- data/lib/asgard/doctor.rb +150 -0
- data/lib/asgard/kernel_methods.rb +12 -6
- data/lib/asgard/tasks.rb +18 -6
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +11 -1
- data/quality.loki +261 -0
- data/quality_rails.loki +46 -0
- data/xyzzy.loki +12 -0
- metadata +17 -17
- data/Rakefile +0 -101
data/docs/api.md
CHANGED
|
@@ -11,7 +11,7 @@ These class methods are defined on the `Asgard` module itself.
|
|
|
11
11
|
| Method | Signature | Description |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| `run!` | `Asgard.run!(argv)` | Main entry point. Finds `.loki`, loads all task files, validates the dependency graph, and dispatches via Thor. Handles its own errors: missing `.loki` and circular dependencies both produce a clean one-line message and `exit 1`. |
|
|
14
|
-
| `find_task_file` | `Asgard.find_task_file →
|
|
14
|
+
| `find_task_file` | `Asgard.find_task_file → Pathname, nil` | Searches `Dir.pwd` and each ancestor directory for a `.loki` file. Returns a `Pathname` of the first match, or `nil` if none is found. |
|
|
15
15
|
|
|
16
16
|
### `run!` Details
|
|
17
17
|
|
|
@@ -35,7 +35,7 @@ These methods are defined as `module_function` on `Kernel` and are therefore ava
|
|
|
35
35
|
|
|
36
36
|
| Method | Signature | Returns | Description |
|
|
37
37
|
|---|---|---|---|
|
|
38
|
-
| `loki_up` | `loki_up(name = ".loki") →
|
|
38
|
+
| `loki_up` | `loki_up(name = ".loki") → Pathname, nil` | `Pathname` or `nil` | Searches `Dir.pwd` and each ancestor directory for a file named `name`. Returns a `Pathname` for the first match, or `nil` if not found. Exact filenames only — does not expand globs. |
|
|
39
39
|
| `import` | `import(path) → true, false` | `true` if any file newly loaded | Loads one `.loki` file or a glob of `.loki` files. Relative paths resolve relative to the caller's file (like `require_relative`). Idempotent via `$LOADED_FEATURES`. Raises `ArgumentError` if `path` does not end with `.loki`. Raises `LoadError` if a non-glob path does not exist. |
|
|
40
40
|
| `import_up` | `import_up(name = ".loki") → true, false` | `true` if any file newly loaded | Combines `loki_up` and `import`. Walks ancestors to find the file or glob match, then loads it. Returns `false` if nothing is found. |
|
|
41
41
|
| `debug?` | `debug? → true, false` | `$DEBUG` | Returns the current value of `$DEBUG`. Set to `true` by `--debug` on the CLI or directly via `$DEBUG = true`. |
|
|
@@ -53,11 +53,11 @@ loki_up(".env") # find the nearest .env file up the tree
|
|
|
53
53
|
loki_up("VERSION") # find a VERSION file in CWD or any ancestor
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Returns
|
|
56
|
+
Returns a `Pathname` or `nil`. Does not load the file. `Pathname` is accepted by `import`, `dotenv`, `load`, and standard Ruby file methods — no `.to_s` conversion needed in common usage.
|
|
57
57
|
|
|
58
58
|
```ruby
|
|
59
59
|
if (path = loki_up("gem_tasks.loki"))
|
|
60
|
-
import path
|
|
60
|
+
import path # Pathname accepted directly
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Pass the located .env to dotenv — works from any subdirectory
|
|
@@ -111,13 +111,62 @@ import_up "*.loki" # find the nearest ancestor with *.loki files
|
|
|
111
111
|
|
|
112
112
|
| Method | Signature | Description |
|
|
113
113
|
|---|---|---|
|
|
114
|
-
| `depends_on` | `depends_on(*tasks)` | Declare prerequisites for the next `def`. Bare symbols run sequentially; arrays within the splat run as a parallel group. |
|
|
114
|
+
| `depends_on` | `depends_on(*tasks)` | Declare prerequisites for the next `def`. Bare symbols run sequentially; arrays within the splat run as a parallel group. A sole `Proc`/lambda defers resolution to `validate_deps!` (after every `.loki` file has loaded) instead of resolving immediately — see [Dynamic Dependencies](dependencies.md#dynamic-dependencies-proc-form). |
|
|
115
115
|
| `dotenv` | `dotenv(path = ".env")` | Load the specified `.env` file into `ENV` using the dotenv gem. Silently skipped if the file does not exist. Called at class-load time. |
|
|
116
|
+
| `header` | `header(text)` | Append a line of text shown above the commands list in `asgard help`. Each call adds another line. No-op for per-command help. |
|
|
117
|
+
| `footer` | `footer(text)` | Prepend a line of text shown below the options block in `asgard help`. Each call inserts above the previous lines. No-op for per-command help. |
|
|
118
|
+
| `no_negate` | `no_negate(*names)` | Suppress `[--no-name]` / `[--skip-name]` help entries for one or more boolean class options. Call after the `class_option` declaration. |
|
|
116
119
|
| `sh` | `sh(script, silent: false)` | Instance method. Run a shell command or multiline heredoc. Single-line → `system(script)`; multiline → `system("bash", "-c", script)`. Exits with the command's status on failure. |
|
|
117
120
|
| `shebang` | `shebang(interpreter, script, silent: false)` | Instance method. Write `script` to a tempfile and execute it with `interpreter`. See the [Shell Helpers](shell.md) page for the full interpreter table. |
|
|
118
|
-
| `validate_deps!` | `Tasks.validate_deps!` | Build and topologically sort the full dependency graph using
|
|
121
|
+
| `validate_deps!` | `Tasks.validate_deps!` | Build and topologically sort the full dependency graph using stdlib `TSort`. Raises `Asgard::CircularDependencyError` on cycles. Called by `run!` at startup. |
|
|
119
122
|
| `_reset_ran!` | `Tasks._reset_ran!` | Clear the per-invocation task deduplication set. Called by `run!` before dispatching. Thread-safe via Mutex. |
|
|
120
123
|
|
|
124
|
+
### `header` and `footer` Accumulation
|
|
125
|
+
|
|
126
|
+
Both `header` and `footer` accumulate across multiple calls and across multiple imported `.loki` files, but they accumulate in opposite directions by design.
|
|
127
|
+
|
|
128
|
+
**`header` appends** — each call adds to the bottom of the header block:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# .loki
|
|
132
|
+
import "quality.loki"
|
|
133
|
+
import "gem_tasks.loki"
|
|
134
|
+
|
|
135
|
+
class Tasks
|
|
136
|
+
header "Project: myapp" # line 1
|
|
137
|
+
header "Root: #{loki_up.parent}" # line 2
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Result in `asgard help`:
|
|
142
|
+
```
|
|
143
|
+
Project: myapp
|
|
144
|
+
Root: /home/user/myapp
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**`footer` prepends** — each call inserts at the top of the footer block:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
class Tasks
|
|
151
|
+
footer "Github: https://github.com/org/myapp" # ends up second
|
|
152
|
+
footer "Docs: https://myapp.example.com" # ends up first
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Result in `asgard help`:
|
|
157
|
+
```
|
|
158
|
+
Docs: https://myapp.example.com
|
|
159
|
+
Github: https://github.com/org/myapp
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Why the asymmetry?** When task files are split across multiple `.loki` files, the base `.loki` is loaded first and any imported files are loaded after. With `header`, earlier-loaded content appears first (the project banner stays at the top). With `footer`, later-loaded content appears first — this allows an imported file to inject a note that appears above the base footer rather than after it.
|
|
163
|
+
|
|
164
|
+
In practice: if only one file calls `header` and one file calls `footer`, the direction doesn't matter. The difference is visible only when multiple `.loki` files both call `header` or both call `footer`.
|
|
165
|
+
|
|
166
|
+
Neither `header` nor `footer` appears when running per-command help (`asgard help <task>`).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
121
170
|
### `depends_on` Argument Shapes
|
|
122
171
|
|
|
123
172
|
```ruby
|
|
@@ -125,8 +174,14 @@ depends_on :build # single sequential dep
|
|
|
125
174
|
depends_on :clean, :build # two sequential deps
|
|
126
175
|
depends_on [:lint, :typecheck] # lint and typecheck run in parallel
|
|
127
176
|
depends_on :setup, [:lint, :build], :test # setup, then lint+build concurrently, then test
|
|
177
|
+
|
|
178
|
+
# A sole Proc/lambda defers resolution to validate_deps!, after every .loki
|
|
179
|
+
# file has loaded — must return the same shape the splat form above would.
|
|
180
|
+
depends_on -> { [all_commands.keys.grep(/_check\z/).map(&:to_sym)] }
|
|
128
181
|
```
|
|
129
182
|
|
|
183
|
+
See [Dynamic Dependencies](dependencies.md#dynamic-dependencies-proc-form) for the full explanation of the Proc form — why it exists, when it runs, and how errors are reported.
|
|
184
|
+
|
|
130
185
|
---
|
|
131
186
|
|
|
132
187
|
## `Tasks` Built-ins
|
|
@@ -137,12 +192,79 @@ depends_on :setup, [:lint, :build], :test # setup, then lint+build concurrently
|
|
|
137
192
|
|---|---|---|
|
|
138
193
|
| `class_option :debug` | class option | `--debug` flag. Sets `$DEBUG = true` before any task runs. Boolean, default `false`. |
|
|
139
194
|
| `class_option :verbose` | class option | `--verbose` flag. Sets `$VERBOSE = true` before any task runs. Boolean, default `false`. |
|
|
140
|
-
| `
|
|
195
|
+
| `class_option :version` | class option | `--version` flag. Handled by `Asgard.run!` before the `.loki` file is loaded — prints `Asgard::VERSION` and exits. `no_negate :version` suppresses the `[--no-version]` / `[--skip-version]` help entries. |
|
|
196
|
+
| `class_option :doctor` | class option | `--doctor` flag. Handled by `Asgard.run!` before the `.loki` file is loaded — runs `Asgard::Doctor.new.run` and exits. `no_negate :doctor` suppresses the `[--no-doctor]` / `[--skip-doctor]` help entries. |
|
|
141
197
|
| `debug?` | Kernel module function | Returns `$DEBUG`. Available everywhere via `Kernel`. |
|
|
142
198
|
| `verbose?` | Kernel module function | Returns `$VERBOSE`. Available everywhere via `Kernel`. |
|
|
143
199
|
|
|
144
200
|
---
|
|
145
201
|
|
|
202
|
+
## `Asgard::Doctor`
|
|
203
|
+
|
|
204
|
+
`asgard --doctor` diagnoses `.loki` resolution, import chains, and task definitions for the current directory. It deliberately bypasses the normal `Tasks` boot sequence used by `run!`, so it can still report findings in situations that would otherwise abort the whole process — a broken `.loki` file, a circular or undefined dependency, or a task silently redefined by a later `def`.
|
|
205
|
+
|
|
206
|
+
| Method | Signature | Description |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| `new` | `Asgard::Doctor.new(dir = Dir.pwd)` | Builds a doctor scoped to `dir`. |
|
|
209
|
+
| `run` | `doctor.run` | Runs the full diagnostic pass and prints a report to stdout. Does not raise or exit — callers (like `Asgard.run!`) decide what to do afterward. |
|
|
210
|
+
| `ancestor_markers` | `Asgard::Doctor.ancestor_markers(dir) → Array<String>` | Every `.loki` marker from `dir` up to the filesystem root, nearest first. Unlike `loki_up`, it collects every match instead of stopping at the first, so shadowed ancestor markers can be reported. |
|
|
211
|
+
| `duplicate_methods` | `Asgard::Doctor.duplicate_methods(method_log) → Hash` | Given an `Asgard::Base` subclass's `_method_log`, returns the subset of entries defined at more than one location — same file or different — which is exactly what a silent `def` override looks like. |
|
|
212
|
+
|
|
213
|
+
Findings fall into three levels: `:info` (what was found — the marker used, each import's result), `:warn` (shadowed markers that are never reached), and `:error` (load failures, redefined methods, dependency graph problems).
|
|
214
|
+
|
|
215
|
+
After the findings, the report prints a **Tasks by file** section: every Thor command, grouped by the `.loki` file it's defined in, as `relative/path:line` — a format an editor can jump straight to. A task name defined at more than one location gets every definition annotated inline: the earlier one(s) are marked `OVERRIDDEN by <file>:<line> — never callable`, and the winning (last) definition is marked `active — redefines <file>:<line>`. This is the exact class of bug the flag was built to catch — a later `def` silently replacing an earlier one, with no error anywhere else in the toolchain. The report ends with a one-line summary and a count of problems/warnings, where each overridden task counts as one problem.
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
asgard --doctor
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The asgard repo ships its own live example of this: `xyzzy.loki` defines a `xyzzy` task, and the top-level `.loki` (which imports it) reopens `Tasks` and defines `xyzzy` again — so the one from `xyzzy.loki` is silently dead. Clone the repo and run `asgard --doctor` from its root to see this for yourself:
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
asgard doctor -- /path/to/asgard
|
|
225
|
+
============================================================
|
|
226
|
+
[INFO] using .loki marker: /path/to/asgard/.loki
|
|
227
|
+
[INFO] import "quality.loki" -> /path/to/asgard/quality.loki
|
|
228
|
+
[INFO] import "gem_tasks.loki" -> /path/to/asgard/gem_tasks.loki
|
|
229
|
+
[INFO] import "git.loki" -> /path/to/asgard/git.loki
|
|
230
|
+
[INFO] import "xyzzy.loki" -> /path/to/asgard/xyzzy.loki
|
|
231
|
+
|
|
232
|
+
Tasks by file:
|
|
233
|
+
|
|
234
|
+
.loki
|
|
235
|
+
xyzzy .loki:15 active — redefines xyzzy.loki:6
|
|
236
|
+
|
|
237
|
+
quality.loki
|
|
238
|
+
test quality.loki:6
|
|
239
|
+
test_verbose quality.loki:15
|
|
240
|
+
quality quality.loki:21
|
|
241
|
+
rubocop quality.loki:34
|
|
242
|
+
rubocop_fix quality.loki:43
|
|
243
|
+
flog_check quality.loki:48
|
|
244
|
+
flay_check quality.loki:82
|
|
245
|
+
reek quality.loki:104
|
|
246
|
+
|
|
247
|
+
gem_tasks.loki
|
|
248
|
+
console gem_tasks.loki:6
|
|
249
|
+
build gem_tasks.loki:16
|
|
250
|
+
install gem_tasks.loki:24
|
|
251
|
+
release gem_tasks.loki:31
|
|
252
|
+
|
|
253
|
+
git.loki
|
|
254
|
+
push git.loki:6
|
|
255
|
+
pull git.loki:9
|
|
256
|
+
fetch git.loki:12
|
|
257
|
+
|
|
258
|
+
xyzzy.loki
|
|
259
|
+
xyzzy xyzzy.loki:6 OVERRIDDEN by .loki:15 — never callable
|
|
260
|
+
============================================================
|
|
261
|
+
1 problem(s), 1 warning(s).
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The warning is a shadowed ancestor `.loki` marker one directory further up the tree — unrelated to the override, and something you may or may not see depending on what's above the repo on your own machine.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
146
268
|
## `Asgard::Base` Internal Class Methods
|
|
147
269
|
|
|
148
270
|
These are implementation details exposed for extensibility. Prefer the DSL methods above in normal use.
|
|
@@ -154,7 +276,6 @@ These are implementation details exposed for extensibility. Prefer the DSL metho
|
|
|
154
276
|
| `_running` | `Set` of task name symbols currently executing (started but not yet finished). |
|
|
155
277
|
| `_cond` | Hash of `ConditionVariable` objects keyed by task name; threads wait here when a dep is in-flight. |
|
|
156
278
|
| `_ran_mutex` | `Mutex` protecting `_done`, `_running`, and `_cond` for thread-safe access. |
|
|
157
|
-
| `_build_dep_graph(stages)` | Translates the stage array (from `_deps`) into a Dagwood-compatible hash. |
|
|
158
279
|
|
|
159
280
|
---
|
|
160
281
|
|
|
@@ -164,7 +285,7 @@ These are implementation details exposed for extensibility. Prefer the DSL metho
|
|
|
164
285
|
|
|
165
286
|
1. Sets `$DEBUG` / `$VERBOSE` from `options` if the corresponding flags are present.
|
|
166
287
|
2. Tries to acquire a run token (`acquire_run_token`): if the task is already in `_done`, returns immediately (skip); if it is in `_running`, waits on the `_cond` ConditionVariable until it finishes, then returns (skip); otherwise adds the task to `_running` and continues.
|
|
167
|
-
3. Resolves dependency stages from `_deps
|
|
288
|
+
3. Resolves dependency stages from `_deps` — already the parallel-group execution plan `depends_on` built — and executes each group in order (parallel groups in threads, sequential groups one at a time).
|
|
168
289
|
4. Calls `command.run(self, *args)` to execute the task itself.
|
|
169
290
|
5. In an `ensure` block, adds the task to `_done` and broadcasts on its `_cond` to wake any waiting threads.
|
|
170
291
|
|
|
@@ -194,7 +315,6 @@ end
|
|
|
194
315
|
| Gem | Version | Purpose |
|
|
195
316
|
|---|---|---|
|
|
196
317
|
| [thor](https://github.com/rails/thor) | `~> 1.0` | CLI framework; provides the full task DSL |
|
|
197
|
-
| [dagwood](https://rubygems.org/gems/dagwood) | `~> 1.0` | DAG library for dependency graph resolution and topological sort |
|
|
198
318
|
| [dotenv](https://github.com/bkeepers/dotenv) | `~> 3.0` | `.env` file loading |
|
|
199
319
|
|
|
200
320
|
---
|
data/docs/changelog.md
CHANGED
|
@@ -8,8 +8,36 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Asg
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`--doctor` built-in CLI flag** — diagnoses `.loki` resolution, import chains, and task definitions for the current directory. Handled directly in `Asgard.run!` (same pattern as `--version`), so it works even when a broken file, a circular/undefined dependency, or a silently redefined task would otherwise abort the whole process. Backed by the new `Asgard::Doctor` class. Includes a "Tasks by file" listing — every command grouped by the file it's defined in, as `file:line`, with any silently-overridden task annotated inline (`OVERRIDDEN by ...` / `active — redefines ...`). See [API Reference](api.md#asgarddoctor).
|
|
14
|
+
- **`helper` DSL method** — defines a method available in both class context (e.g. inside `header`) and instance context (inside task methods) with a single declaration. Eliminates the manual `def self.name` + `no_commands { private def name = self.class.name }` boilerplate. Supports positional arguments, keyword arguments, and block arguments. See [Helper Methods](helpers.md).
|
|
15
|
+
- **Flay and Reek quality gates** — `flay_check` checks for structural duplication (mass ≥ 150); `reek` checks code smells. Reviewed findings are grandfathered precisely, per method and detector, via `reek --todo`-generated `exclude:` entries in `.reek.yml` — a genuinely new smell still fails the gate even at an already-reviewed method.
|
|
16
|
+
- **`test_verbose`, `console` tasks** — verbose test output and an IRB console with the gem loaded.
|
|
17
|
+
- **`git.loki`** — per-repo `push`/`pull`/`fetch` tasks.
|
|
18
|
+
- **`typos_check` / `typos_fix` tasks** — spell-checking via the external `typos` CLI (`brew install typos-cli`). Reports `SKIP` (not a failure) if `typos` isn't installed, with a one-line install hint.
|
|
19
|
+
- **`fasterer_check` task** — performance-idiom suggestions from the `fasterer` gem, reported as `WARN` (non-blocking).
|
|
20
|
+
- **`SKIP` / `WARN` quality-gate statuses** — alongside `PASS`/`FAIL`; only `FAIL` blocks `quality`.
|
|
21
|
+
- Every quality gate now writes full detail to a `<gate>_output.txt` file (gitignored) and prints just a one-line summary to stdout.
|
|
22
|
+
- **`asgard tree`** now shows the project header/footer, matching `asgard help`.
|
|
23
|
+
- **`bundler_audit_check` task** — `bundle-audit check --update` against `Gemfile.lock`; `FAIL` on any known vulnerability.
|
|
24
|
+
- **`quality_rails.loki`** — imported only when `Rails` is defined; ships `brakeman_check` as a Rails security-scan example. No wiring needed — `quality` discovers it automatically (see `depends_on` below).
|
|
25
|
+
- **`depends_on` accepts a Proc/lambda**, not just a fixed list — resolved once, in `validate_deps!`, after every `.loki` file has loaded, instead of immediately. Solves "load order matters" for a dependency list that can't be known upfront, e.g. every task ending in `_check` across several files. See [Dynamic Dependencies](dependencies.md#dynamic-dependencies-proc-form).
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **`quality` task** — discovers every `*_check` task at run time (via a `depends_on` Proc) rather than a fixed list, and runs them all in parallel with a colorized PASS/FAIL/WARN/SKIP summary and tally.
|
|
30
|
+
- **`test`, `rubocop`, `reek` renamed to `test_check`, `rubocop_check`, `reek_check`** — consistency with the other gates is what makes `quality`'s automatic discovery possible.
|
|
31
|
+
- **`release` task** — prompts for confirmation unless `-y`/`--yes` is passed.
|
|
32
|
+
- **`Asgard::Base` and `Asgard::Doctor` split into mixins** — `lib/asgard/base/{registry,dependency_graph,task_dsl,dispatch}.rb` and `lib/asgard/doctor/{task_sections,report}.rb`. No behavior change; drops both classes' Reek `TooManyMethods`/`TooManyInstanceVariables` warnings to zero.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **`bin/asgard`** — switched from `require "asgard"` to `require_relative "../lib/asgard"` so the executable always loads the library shipped alongside it, instead of whatever `asgard` gem happens to be installed separately.
|
|
37
|
+
|
|
11
38
|
### Removed
|
|
12
39
|
|
|
40
|
+
- **`reek_baseline` / `ensure_quality_dir` tasks and `.quality/`** — superseded by precise per-method `exclude:` entries in `.reek.yml` (see the Reek gate entry above).
|
|
13
41
|
- **`var` DSL method** — replaced by native Ruby class variables. Use `@@name ||= "value".freeze` in the class body. Class variables are visible in all task instance methods and in subcommand subclasses, making them the correct tool for shared configuration in a Thor-based task runner. See [Variables](variables.md).
|
|
14
42
|
|
|
15
43
|
## [0.2.0] — 2026-05-29
|
data/docs/dependencies.md
CHANGED
|
@@ -145,9 +145,54 @@ When `asgard ci` runs, `setup` executes once even though both `test` and `lint`
|
|
|
145
145
|
|
|
146
146
|
---
|
|
147
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
|
+
|
|
148
193
|
## Circular Dependency Detection
|
|
149
194
|
|
|
150
|
-
Asgard validates the full dependency graph using [
|
|
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:
|
|
151
196
|
|
|
152
197
|
```ruby
|
|
153
198
|
class Tasks
|
|
@@ -187,7 +232,43 @@ class Tasks
|
|
|
187
232
|
end
|
|
188
233
|
```
|
|
189
234
|
|
|
190
|
-
|
|
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 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, 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
|
+
|
|
251
|
+
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 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)`.
|
|
252
|
+
|
|
253
|
+
**Shape:** the Proc must return exactly what the plain-array form would receive as its splat arguments — an array of stages, each a `Symbol` (sequential) or `Array` (parallel group). 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]`.
|
|
254
|
+
|
|
255
|
+
**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 in the dependency table, so cycle detection, undefined-task checks, and arity checks all run against the *resolved* list — a Proc that references an undefined task, or that itself introduces a cycle, is caught at startup exactly like a plain array would be:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
asgard quality
|
|
259
|
+
# asgard: undefined task(s) in depends_on: ghost_check
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**`self` inside the Proc:** since the Proc is 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!`.
|
|
263
|
+
|
|
264
|
+
**If the Proc raises**, the error is caught and re-raised as `Asgard::Error` naming the task it was declared for:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
asgard: depends_on proc for 'quality' raised RuntimeError: boom
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
!!! tip
|
|
271
|
+
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.
|
|
191
272
|
|
|
192
273
|
---
|
|
193
274
|
|
data/docs/getting-started.md
CHANGED
|
@@ -138,24 +138,26 @@ myproject/
|
|
|
138
138
|
qa.loki ← test and lint tasks
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
Each `*.loki` file reopens `class Tasks`. To load them,
|
|
141
|
+
Each `*.loki` file reopens `class Tasks`. To load them, call `import "*.loki"` at the top of `.loki` — files are loaded in alphabetical order. See [Task Files](task-files.md) for full details.
|
|
142
142
|
|
|
143
143
|
---
|
|
144
144
|
|
|
145
145
|
## Built-in Flags
|
|
146
146
|
|
|
147
|
-
Every task automatically has
|
|
147
|
+
Every task automatically has four flags available, defined as `class_option` on `Tasks`:
|
|
148
148
|
|
|
149
149
|
| Flag | Description |
|
|
150
150
|
|---|---|
|
|
151
151
|
| `--version` | Print the Asgard version and exit |
|
|
152
152
|
| `--debug` | Set `$DEBUG = true` before the task runs |
|
|
153
153
|
| `--verbose` | Set `$VERBOSE = true` before the task runs |
|
|
154
|
+
| `--doctor` | Diagnose `.loki` resolution, imports, and task definitions for the CWD, then exit |
|
|
154
155
|
|
|
155
156
|
```bash
|
|
156
157
|
asgard --version
|
|
157
158
|
asgard hello --debug
|
|
158
159
|
asgard hello --verbose
|
|
160
|
+
asgard --doctor
|
|
159
161
|
```
|
|
160
162
|
|
|
161
163
|
Inside a task body, use the `debug?` and `verbose?` predicates:
|
data/docs/helpers.md
CHANGED
|
@@ -75,6 +75,90 @@ end
|
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
+
## The `helper` DSL Method
|
|
79
|
+
|
|
80
|
+
`helper` is an Asgard DSL method that defines a helper available in **both class context and instance context** with a single declaration. It is the right tool when a value needs to be used inside a `header` or `footer` call (which execute at class load time) and also inside task instance methods.
|
|
81
|
+
|
|
82
|
+
### The problem it solves
|
|
83
|
+
|
|
84
|
+
Thor task methods run as instance methods. Class-level DSL calls like `header` and `footer` run as class methods. A plain `def` only creates an instance method, so it cannot be called inside `header`. Conversely, `def self.name` only creates a class method, so it cannot be called inside a task body without `self.class.name`.
|
|
85
|
+
|
|
86
|
+
The manual workaround is verbose:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class Tasks
|
|
90
|
+
@@project ||= "myapp".freeze
|
|
91
|
+
|
|
92
|
+
# class method for header/footer
|
|
93
|
+
def self.version
|
|
94
|
+
@@version ||= File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# private instance method delegating to the class method
|
|
98
|
+
no_commands do
|
|
99
|
+
private def version = self.class.version
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
header "#{@@project} v#{version}"
|
|
103
|
+
|
|
104
|
+
desc "Show version"
|
|
105
|
+
def show_version = puts version
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`helper` replaces those six lines with one:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
class Tasks
|
|
113
|
+
@@project ||= "myapp".freeze
|
|
114
|
+
|
|
115
|
+
helper(:version) {
|
|
116
|
+
@@version ||= File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
header "#{@@project} v#{version}"
|
|
120
|
+
|
|
121
|
+
desc "Show version"
|
|
122
|
+
def show_version = puts version
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Arguments
|
|
127
|
+
|
|
128
|
+
`helper` supports any argument signature valid in a Ruby method definition: positional, keyword, default values, and blocks.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# No arguments
|
|
132
|
+
helper(:project_root) { loki_up.parent.to_s }
|
|
133
|
+
|
|
134
|
+
# Positional arguments
|
|
135
|
+
helper(:gem_path) { |name| "lib/#{name}/version.rb" }
|
|
136
|
+
|
|
137
|
+
# Positional with default
|
|
138
|
+
helper(:tag_prefix) { |sep = "-"| "#{@@project}#{sep}" }
|
|
139
|
+
|
|
140
|
+
# Positional and keyword arguments
|
|
141
|
+
helper(:format_version) { |name, version, prefix: "v", separator: "-"|
|
|
142
|
+
"#{prefix}#{name}#{separator}#{version}"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Wrong argument counts or unknown keyword names raise the same `ArgumentError` Ruby raises for any method call — no special error handling needed.
|
|
147
|
+
|
|
148
|
+
### Visibility
|
|
149
|
+
|
|
150
|
+
`helper`-defined methods are:
|
|
151
|
+
|
|
152
|
+
- **Excluded from `asgard help`** — they never appear as commands
|
|
153
|
+
- **Blocked from CLI invocation** — cannot be called directly from the command line
|
|
154
|
+
- **Private on the instance side** — not accessible from outside the class
|
|
155
|
+
|
|
156
|
+
### When to use `helper`
|
|
157
|
+
|
|
158
|
+
Use `helper` when the value or computation must be available in both a class-level DSL call (`header`, `footer`, a `@@var` initializer) and inside task instance methods. For helpers that are only needed inside task bodies, a plain `private` method is simpler.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
78
162
|
## Choosing Between `private` and `no_commands`
|
|
79
163
|
|
|
80
164
|
| | `private` | `no_commands` |
|
data/docs/index.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<li><strong>Dotenv Support</strong> — load <code>.env</code> files into the environment with <code>dotenv</code></li>
|
|
19
19
|
<li><strong>Auto-Discovery</strong> — <code>.loki</code> root marker searched from CWD upward through parent directories</li>
|
|
20
20
|
<li><strong>Multi-File Tasks</strong> — split tasks across <code>*.loki</code> files loaded via <code>import</code></li>
|
|
21
|
-
<li><strong>Built-in Flags</strong> — <code>--version</code>, <code>--debug</code>, and <code>--
|
|
21
|
+
<li><strong>Built-in Flags</strong> — <code>--version</code>, <code>--debug</code>, <code>--verbose</code>, and <code>--doctor</code> available on every task</li>
|
|
22
22
|
</ul>
|
|
23
23
|
</td>
|
|
24
24
|
</tr>
|
|
@@ -72,7 +72,7 @@ The full Thor DSL is available: `desc`, `method_option`, `class_option`, `long_d
|
|
|
72
72
|
| [Subcommands](subcommands.md) | Grouping tasks under a namespace |
|
|
73
73
|
| [Shell Helpers](shell.md) | `sh`, `shebang`, and supported interpreters |
|
|
74
74
|
| [Environment](environment.md) | Loading `.env` files with `dotenv` |
|
|
75
|
-
| [Task Files](task-files.md) | `.loki` root marker,
|
|
75
|
+
| [Task Files](task-files.md) | `.loki` root marker, `import`, multi-file layout |
|
|
76
76
|
| [API Reference](api.md) | Module methods, DSL methods, error classes |
|
|
77
77
|
| [Examples](examples.md) | Working `.loki` files for every feature |
|
|
78
78
|
| [Changelog](changelog.md) | Release history |
|
|
@@ -82,4 +82,4 @@ The full Thor DSL is available: `desc`, `method_option`, `class_option`, `long_d
|
|
|
82
82
|
## Requirements
|
|
83
83
|
|
|
84
84
|
- Ruby >= 3.2.0
|
|
85
|
-
- Dependencies: [thor](https://github.com/rails/thor) `~> 1.0`, [
|
|
85
|
+
- Dependencies: [thor](https://github.com/rails/thor) `~> 1.0`, [dotenv](https://github.com/bkeepers/dotenv) `~> 3.0`
|
data/docs/options.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Options & Flags
|
|
2
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
|
|
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
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -71,21 +71,65 @@ end
|
|
|
71
71
|
|
|
72
72
|
Both `deploy` and `migrate` automatically accept `--dry-run` and `--env`.
|
|
73
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
|
+
|
|
74
108
|
---
|
|
75
109
|
|
|
76
110
|
## Built-in Flags
|
|
77
111
|
|
|
78
|
-
`Tasks` ships with
|
|
112
|
+
`Tasks` ships with four built-in `class_option` declarations — `--debug`, `--verbose`, `--version`, and `--doctor` — all visible in the Options section of `asgard help`.
|
|
79
113
|
|
|
80
114
|
### `--version`
|
|
81
115
|
|
|
82
|
-
Prints `Asgard::VERSION` and exits.
|
|
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):
|
|
83
117
|
|
|
84
118
|
```bash
|
|
85
119
|
asgard --version
|
|
86
|
-
# 0.
|
|
120
|
+
# 0.3.0
|
|
87
121
|
```
|
|
88
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
|
+
|
|
89
133
|
### `--debug`
|
|
90
134
|
|
|
91
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`):
|
|
@@ -173,8 +217,8 @@ asgard deploy production --verbose
|
|
|
173
217
|
Methods whose names start with `_` are considered gem-owned in Asgard's naming convention. `run!` guards against invoking them directly from the CLI:
|
|
174
218
|
|
|
175
219
|
```bash
|
|
176
|
-
asgard
|
|
177
|
-
# asgard: unknown command '
|
|
220
|
+
asgard _something
|
|
221
|
+
# asgard: unknown command '_something'
|
|
178
222
|
```
|
|
179
223
|
|
|
180
|
-
If you define your own methods on `Tasks`, avoid the `_` prefix to prevent them from being
|
|
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/task-files.md
CHANGED
|
@@ -93,7 +93,7 @@ import("gem_tasks.loki") ? "loaded now" : "already loaded"
|
|
|
93
93
|
|
|
94
94
|
## Finding Files with `loki_up`
|
|
95
95
|
|
|
96
|
-
`loki_up(name = ".loki")` searches `Dir.pwd` and each ancestor directory for a file with the given name, returning
|
|
96
|
+
`loki_up(name = ".loki")` searches `Dir.pwd` and each ancestor directory for a file with the given name, returning a `Pathname` or `nil`. It does **not** load the file — it only finds it.
|
|
97
97
|
|
|
98
98
|
Despite the name, `loki_up` is not limited to `.loki` files — it will locate any file by name. This makes it useful for finding shared config files, `.env` files, or any other resource that lives somewhere up the directory tree:
|
|
99
99
|
|
|
@@ -401,7 +401,7 @@ end
|
|
|
401
401
|
|
|
402
402
|
| Method | Finds? | Loads? | Glob? | Ancestor search? |
|
|
403
403
|
|---|---|---|---|---|
|
|
404
|
-
| `loki_up(name)` | Yes | No | No | Yes |
|
|
404
|
+
| `loki_up(name)` | Yes (`Pathname`) | No | No | Yes |
|
|
405
405
|
| `import(path)` | No | Yes | Yes | No |
|
|
406
406
|
| `import_up(name)` | Yes | Yes | Yes | Yes |
|
|
407
407
|
| Asgard's `run!` | Yes | `.loki` only | No | Yes |
|