asgard 0.1.2 → 0.3.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/.github/workflows/deploy-github-pages.yml +52 -0
- data/.loki +7 -9
- data/.rubocop.yml +157 -0
- data/CHANGELOG.md +93 -2
- data/CLAUDE.md +19 -9
- data/README.md +110 -58
- data/Rakefile +83 -4
- data/docs/api.md +204 -0
- data/docs/assets/css/custom.css +93 -0
- data/docs/assets/images/asgard.jpg +0 -0
- data/docs/changelog.md +104 -0
- data/docs/dependencies.md +221 -0
- data/docs/environment.md +129 -0
- data/docs/examples.md +140 -0
- data/docs/getting-started.md +179 -0
- data/docs/helpers.md +178 -0
- data/docs/index.md +85 -0
- data/docs/options.md +180 -0
- data/docs/shell.md +208 -0
- data/docs/subcommands.md +181 -0
- data/docs/task-files.md +407 -0
- data/docs/tasks.md +286 -0
- data/docs/variables.md +338 -0
- data/examples/.env +4 -0
- data/examples/.loki +24 -2
- data/examples/concurrent.loki +58 -0
- data/examples/db_subcommands.loki +3 -3
- data/examples/env_usage.loki +27 -0
- data/examples/kitchen_sink.loki +48 -15
- data/examples/server_subcommands.loki +3 -3
- data/examples/subdir/.loki +12 -0
- data/examples/subdir/import_demo.loki +14 -0
- data/examples/subdir/import_up_demo.loki +18 -0
- data/lib/asgard/base.rb +159 -54
- data/lib/asgard/kernel_methods.rb +77 -0
- data/lib/asgard/shell.rb +9 -6
- data/lib/asgard/tasks.rb +0 -5
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +7 -18
- data/mkdocs.yml +164 -0
- metadata +32 -4
data/docs/api.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
This page documents the public Ruby API for the Asgard gem. Most users interact with Asgard through the CLI and the task DSL — this page is primarily useful when integrating Asgard into tooling or extending it programmatically.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## `Asgard` Module Methods
|
|
8
|
+
|
|
9
|
+
These class methods are defined on the `Asgard` module itself.
|
|
10
|
+
|
|
11
|
+
| Method | Signature | Description |
|
|
12
|
+
|---|---|---|
|
|
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 → String, nil` | Searches `Dir.pwd` and each ancestor directory for a `.loki` file. Returns the absolute path string of the first match, or `nil` if none is found. |
|
|
15
|
+
|
|
16
|
+
### `run!` Details
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
Asgard.run!(ARGV)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`run!` guards against direct invocation of `_`-prefixed commands before any files are loaded:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
abort "asgard: unknown command '#{argv.first}'" if argv.first&.start_with?("_")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
After loading task files, it calls `Tasks.validate_deps!` (circular dependency check) and `Tasks._reset_ran!` (clears per-invocation deduplication state) before starting Thor.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Kernel Methods
|
|
33
|
+
|
|
34
|
+
These methods are defined as `module_function` on `Kernel` and are therefore available everywhere in Ruby — at the top level of `.loki` files, inside class bodies, and inside task method bodies. No `require` or `include` is needed; they are loaded when `asgard` starts.
|
|
35
|
+
|
|
36
|
+
| Method | Signature | Returns | Description |
|
|
37
|
+
|---|---|---|---|
|
|
38
|
+
| `loki_up` | `loki_up(name = ".loki") → String, nil` | Absolute path or `nil` | Searches `Dir.pwd` and each ancestor directory for a file named `name`. Returns the first match's absolute path, or `nil` if not found. Exact filenames only — does not expand globs. |
|
|
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
|
+
| `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
|
+
| `debug?` | `debug? → true, false` | `$DEBUG` | Returns the current value of `$DEBUG`. Set to `true` by `--debug` on the CLI or directly via `$DEBUG = true`. |
|
|
42
|
+
| `verbose?` | `verbose? → true, false` | `$VERBOSE` | Returns the current value of `$VERBOSE`. Set to `true` by `--verbose` on the CLI or directly via `$VERBOSE = true`. |
|
|
43
|
+
| `env` | `env(name, default = nil) → String, nil` | `ENV` value or default | Fetches an environment variable by symbol or string name. The name is upcased automatically. Raises `KeyError` when the variable is missing and no default is given. |
|
|
44
|
+
|
|
45
|
+
### `loki_up` Details
|
|
46
|
+
|
|
47
|
+
Despite the name, `loki_up` is not limited to `.loki` files — it locates any file by walking up the directory tree:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
loki_up # find .loki (the project root marker)
|
|
51
|
+
loki_up("gem_tasks.loki") # find gem_tasks.loki in CWD or any ancestor
|
|
52
|
+
loki_up(".env") # find the nearest .env file up the tree
|
|
53
|
+
loki_up("VERSION") # find a VERSION file in CWD or any ancestor
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Returns an absolute path string or `nil`. Does not load the file.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
if (path = loki_up("gem_tasks.loki"))
|
|
60
|
+
import path
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Pass the located .env to dotenv — works from any subdirectory
|
|
64
|
+
dotenv loki_up(".env") || ".env"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `import` Details
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
import "build.loki" # relative — resolved from the calling file's directory
|
|
71
|
+
import "/home/shared/gem_tasks.loki" # absolute
|
|
72
|
+
import "*.loki" # all *.loki in the same directory as the caller
|
|
73
|
+
import "../shared/*.loki" # all *.loki one level up
|
|
74
|
+
import "**/*.loki" # all *.loki recursively
|
|
75
|
+
import Pathname.new("tasks.loki") # Pathname accepted
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Extension requirement:** the path (or glob pattern) must end with `.loki`. Passing any other extension raises `ArgumentError`.
|
|
79
|
+
|
|
80
|
+
**Glob behaviour:** `Dir.glob` is used for pattern expansion. `*.loki` does not match `.loki` (the dotfile) — Ruby's glob excludes dotfiles from `*` by default. Files are loaded in the order `Dir.glob` returns them (sorted on Ruby ≥ 2.7).
|
|
81
|
+
|
|
82
|
+
**Idempotency:** each resolved absolute path is checked against `$LOADED_FEATURES` before loading. A file already in `$LOADED_FEATURES` is silently skipped and contributes `false` to the return value.
|
|
83
|
+
|
|
84
|
+
**Return value:** `true` if at least one file was newly loaded; `false` if all matched files were already loaded or no glob pattern produced any matches.
|
|
85
|
+
|
|
86
|
+
**Verbose/debug output** (to stderr):
|
|
87
|
+
- `verbose?` true — prints each file path as it is loaded
|
|
88
|
+
- `debug?` true — also prints a skip message for each already-loaded file
|
|
89
|
+
|
|
90
|
+
### `import_up` Details
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
import_up # find and load .loki
|
|
94
|
+
import_up "gem_tasks.loki" # find and load gem_tasks.loki up the tree
|
|
95
|
+
import_up "*.loki" # find the nearest ancestor with *.loki files and load them all
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Exact name:** delegates to `loki_up` to find the file, then calls `import` with the absolute path. Returns `false` without raising if the file is not found.
|
|
99
|
+
|
|
100
|
+
**Glob name:** walks ancestor directories manually using `Dir.glob`. Stops at the **first** ancestor that has any matches and loads all of them — it does not continue walking after finding a match. Returns `false` if no ancestor contains matching files.
|
|
101
|
+
|
|
102
|
+
**Verbose/debug output** (to stderr):
|
|
103
|
+
- `verbose?` true — prints `name → /full/path` when a file or directory is found
|
|
104
|
+
- `debug?` true — also prints `name not found` when the search comes up empty
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## `Asgard::Base` DSL Class Methods
|
|
109
|
+
|
|
110
|
+
`Asgard::Base` is a `Thor` subclass that provides the task DSL. It is the superclass of `Tasks`. All DSL methods are class methods (called in the class body).
|
|
111
|
+
|
|
112
|
+
| Method | Signature | Description |
|
|
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. |
|
|
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
|
+
| `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
|
+
| `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 Dagwood. Raises `Asgard::CircularDependencyError` on cycles. Called by `run!` at startup. |
|
|
119
|
+
| `_reset_ran!` | `Tasks._reset_ran!` | Clear the per-invocation task deduplication set. Called by `run!` before dispatching. Thread-safe via Mutex. |
|
|
120
|
+
|
|
121
|
+
### `depends_on` Argument Shapes
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
depends_on :build # single sequential dep
|
|
125
|
+
depends_on :clean, :build # two sequential deps
|
|
126
|
+
depends_on [:lint, :typecheck] # lint and typecheck run in parallel
|
|
127
|
+
depends_on :setup, [:lint, :build], :test # setup, then lint+build concurrently, then test
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## `Tasks` Built-ins
|
|
133
|
+
|
|
134
|
+
`Tasks` is pre-defined by the gem as `class Tasks < Asgard::Base`. It adds the following:
|
|
135
|
+
|
|
136
|
+
| Item | Type | Description |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `class_option :debug` | class option | `--debug` flag. Sets `$DEBUG = true` before any task runs. Boolean, default `false`. |
|
|
139
|
+
| `class_option :verbose` | class option | `--verbose` flag. Sets `$VERBOSE = true` before any task runs. Boolean, default `false`. |
|
|
140
|
+
| `_version` | private task method | Implements `--version`. Prints `Asgard::VERSION` and exits. Registered via `map "--version" => :_version`. Uses `_` prefix convention. |
|
|
141
|
+
| `debug?` | Kernel module function | Returns `$DEBUG`. Available everywhere via `Kernel`. |
|
|
142
|
+
| `verbose?` | Kernel module function | Returns `$VERBOSE`. Available everywhere via `Kernel`. |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## `Asgard::Base` Internal Class Methods
|
|
147
|
+
|
|
148
|
+
These are implementation details exposed for extensibility. Prefer the DSL methods above in normal use.
|
|
149
|
+
|
|
150
|
+
| Method | Description |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `_deps` | Hash mapping task name symbols to their stage arrays. Set by `depends_on` + `method_added`. |
|
|
153
|
+
| `_done` | `Set` of task name symbols that have completed in the current invocation. |
|
|
154
|
+
| `_running` | `Set` of task name symbols currently executing (started but not yet finished). |
|
|
155
|
+
| `_cond` | Hash of `ConditionVariable` objects keyed by task name; threads wait here when a dep is in-flight. |
|
|
156
|
+
| `_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
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## `invoke_command` Hook
|
|
162
|
+
|
|
163
|
+
`Asgard::Base` overrides Thor's `invoke_command` to implement dependency resolution and deduplication:
|
|
164
|
+
|
|
165
|
+
1. Sets `$DEBUG` / `$VERBOSE` from `options` if the corresponding flags are present.
|
|
166
|
+
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`, builds the Dagwood graph, and executes groups (parallel groups in threads, sequential groups one at a time).
|
|
168
|
+
4. Calls `command.run(self, *args)` to execute the task itself.
|
|
169
|
+
5. In an `ensure` block, adds the task to `_done` and broadcasts on its `_cond` to wake any waiting threads.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Error Classes
|
|
174
|
+
|
|
175
|
+
| Class | Superclass | Description |
|
|
176
|
+
|---|---|---|
|
|
177
|
+
| `Asgard::Error` | `StandardError` | Base error class for all Asgard errors. |
|
|
178
|
+
| `Asgard::CircularDependencyError` | `Asgard::Error` | Raised by `validate_deps!` when a cycle is detected in the dependency graph. `run!` catches this and calls `abort` with a clean message. |
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
begin
|
|
182
|
+
Asgard.run!(ARGV)
|
|
183
|
+
rescue Asgard::CircularDependencyError => e
|
|
184
|
+
# This is already handled inside run! — you only need this
|
|
185
|
+
# if you call validate_deps! directly in your own tooling.
|
|
186
|
+
abort "circular dependency: #{e.message}"
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Dependencies
|
|
193
|
+
|
|
194
|
+
| Gem | Version | Purpose |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| [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
|
+
| [dotenv](https://github.com/bkeepers/dotenv) | `~> 3.0` | `.env` file loading |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Ruby Version Requirement
|
|
203
|
+
|
|
204
|
+
Asgard requires **Ruby >= 3.2.0**.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Asgard documentation — custom styles
|
|
3
|
+
Material theme handles the heavy lifting; this file adds targeted polish.
|
|
4
|
+
========================================================================== */
|
|
5
|
+
|
|
6
|
+
/* --------------------------------------------------------------------------
|
|
7
|
+
.loki filename display
|
|
8
|
+
Used whenever the filename ".loki" or "*.loki" appears inline or in code.
|
|
9
|
+
-------------------------------------------------------------------------- */
|
|
10
|
+
|
|
11
|
+
/* Give inline code that looks like a .loki filename a subtle Norse-gold tint */
|
|
12
|
+
code:is([class*="language-"]) .token.string:has-text(".loki"),
|
|
13
|
+
.md-typeset code.loki-file {
|
|
14
|
+
color: var(--md-accent-fg-color);
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Highlight .loki filenames in directory tree code blocks */
|
|
19
|
+
.md-typeset .highlight .filename {
|
|
20
|
+
background-color: color-mix(in srgb, var(--md-primary-fg-color) 12%, transparent);
|
|
21
|
+
border-bottom: 2px solid var(--md-accent-fg-color);
|
|
22
|
+
border-radius: 4px 4px 0 0;
|
|
23
|
+
color: var(--md-default-fg-color);
|
|
24
|
+
font-size: 0.75rem;
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
letter-spacing: 0.04em;
|
|
27
|
+
padding: 0.3rem 0.8rem;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* --------------------------------------------------------------------------
|
|
31
|
+
Admonition tweaks — slightly warmer warning border for the "argument"
|
|
32
|
+
class-level scope warning that appears on the tasks page.
|
|
33
|
+
-------------------------------------------------------------------------- */
|
|
34
|
+
|
|
35
|
+
.md-typeset .admonition.warning,
|
|
36
|
+
.md-typeset details.warning {
|
|
37
|
+
border-left-color: #e6a817;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.md-typeset .admonition.warning > .admonition-title,
|
|
41
|
+
.md-typeset details.warning > summary {
|
|
42
|
+
background-color: rgba(230, 168, 23, 0.12);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* --------------------------------------------------------------------------
|
|
46
|
+
Table of Contents — emphasise the current section a touch more
|
|
47
|
+
-------------------------------------------------------------------------- */
|
|
48
|
+
|
|
49
|
+
.md-nav__link--active {
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* --------------------------------------------------------------------------
|
|
54
|
+
Home page feature table (HTML table in index.md)
|
|
55
|
+
-------------------------------------------------------------------------- */
|
|
56
|
+
|
|
57
|
+
.md-typeset table:not([class]) td {
|
|
58
|
+
vertical-align: top;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* --------------------------------------------------------------------------
|
|
62
|
+
Execution diagram in dependencies.md — keep it tight and readable
|
|
63
|
+
-------------------------------------------------------------------------- */
|
|
64
|
+
|
|
65
|
+
.md-typeset pre code {
|
|
66
|
+
font-size: 0.85em;
|
|
67
|
+
line-height: 1.5;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* --------------------------------------------------------------------------
|
|
71
|
+
Subtle Norse-shield watermark on the hero block (index page only).
|
|
72
|
+
Relies on the Material "primary: indigo" palette.
|
|
73
|
+
-------------------------------------------------------------------------- */
|
|
74
|
+
|
|
75
|
+
.md-header {
|
|
76
|
+
box-shadow: 0 2px 8px rgba(63, 81, 181, 0.25);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* --------------------------------------------------------------------------
|
|
80
|
+
Code annotations — keep them readable across both light and dark palettes
|
|
81
|
+
-------------------------------------------------------------------------- */
|
|
82
|
+
|
|
83
|
+
.md-typeset .md-annotation__index > * {
|
|
84
|
+
font-size: 0.7rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* --------------------------------------------------------------------------
|
|
88
|
+
Task-runner specific: distinguish shell prompt lines from output lines
|
|
89
|
+
in bash code blocks by dimming lines that don't start with '#' or 'asgard'
|
|
90
|
+
-------------------------------------------------------------------------- */
|
|
91
|
+
|
|
92
|
+
/* (Future: add targeted styles once MkDocs Material supports per-line
|
|
93
|
+
highlighting via config; for now this is intentionally minimal.) */
|
|
Binary file
|
data/docs/changelog.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Asgard are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Asgard adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- **`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
|
+
|
|
15
|
+
## [0.2.0] — 2026-05-29
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `*.loki` files are no longer auto-loaded by default. Pass `--auto-load` to `asgard` to load all `*.loki` files from the project root alphabetically before `.loki`. This is a breaking change for projects using the multi-file layout.
|
|
20
|
+
- Added `--auto-load` as a built-in CLI flag in `Tasks`, visible in `asgard help`
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [0.1.2] — 2026-05-29
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `--version` built-in CLI flag — prints `Asgard::VERSION` and exits; implemented as a `_`-prefixed method in `Tasks` per the gem-owned naming convention
|
|
29
|
+
- `--debug` and `--verbose` built-in `class_option` declarations on `Tasks` — set `$DEBUG`/`$VERBOSE` before any task runs via the `invoke_command` hook in `Asgard::Base`
|
|
30
|
+
- `debug?` and `verbose?` private predicate helpers on `Tasks` — thin wrappers around `$DEBUG` and `$VERBOSE` for use inside task bodies
|
|
31
|
+
- `_` prefix convention for gem-owned methods in `Tasks` — built-in methods use `_` prefix to distinguish them from user-defined tasks
|
|
32
|
+
- `run!` guards against direct invocation of `_`-prefixed commands with a clean error message and exit 1
|
|
33
|
+
- `examples/` directory with working `.loki` files:
|
|
34
|
+
- `kitchen_sink.loki` — demonstrates the full Thor DSL (all option types, `long_desc`, `class_option`, `default_task`, `map`, `depends_on`, `var`, `no_commands`, `private`)
|
|
35
|
+
- `server_subcommands.loki` — subcommand group for server management
|
|
36
|
+
- `db_subcommands.loki` — subcommand group for database management with `depends_on` chaining
|
|
37
|
+
- `concurrent.loki` — demonstrates parallel task execution with interleaved thread output
|
|
38
|
+
- README sections: Helper methods, Subcommands, Thor wrapper callout
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Replaced `warn`/`exit 1` with `abort` throughout `run!` — `Kernel#warn` is silenced when `$VERBOSE = nil`, which is the default in Ruby 4.0; `abort` writes to `$stderr` regardless
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `--debug` and `--verbose` promoted from mapped tasks to `class_option` — they now work as modifiers alongside other commands (e.g. `asgard build --debug`) rather than as standalone commands
|
|
47
|
+
- Removed all references to `just` task runner and `recipe` terminology; Asgard uses "task" throughout
|
|
48
|
+
- `depends_on` parameter renamed from `*recipes` to `*tasks` for consistency
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## [0.1.1] — 2026-05-28
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- Parallel dependency execution — wrap deps in an array to run them concurrently:
|
|
57
|
+
`depends_on [:build, :lint]` or `depends_on :setup, [:build, :lint], :deploy`
|
|
58
|
+
- `Asgard.run!(argv)` — single entry point encapsulating find, load, validate, and start
|
|
59
|
+
- `Asgard.load_loki(dir)` — auto-loads all `*.loki` files in a directory alphabetically
|
|
60
|
+
- `Tasks` class pre-defined by the gem (`class Tasks < Asgard::Base`) — task files reopen it without restating the superclass
|
|
61
|
+
- `lib/asgard/tasks.rb` — ships the pre-defined `Tasks` class
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- Replaced `SimpleFlow` dependency with `Dagwood` — purpose-built DAG library with no extra dependencies and no Ruby 4 compatibility issues
|
|
66
|
+
- `bin/asgard` simplified to two lines: `require "asgard"` + `Asgard.run!(ARGV)`
|
|
67
|
+
- Task file convention: `.loki` is the project root marker and entry point; `*.loki` files each reopen `class Tasks` and are auto-loaded before `.loki`
|
|
68
|
+
- `Asgard.find_task_files` renamed to `Asgard.find_task_file` (singular — only `.loki` is the entry point)
|
|
69
|
+
- `depends_on` now accepts mixed sequential/parallel stages; bare symbols run sequentially, arrays within the splat run in parallel
|
|
70
|
+
- `run!` handles its own errors — missing `.loki` and circular dependencies produce a clean one-line message and exit 1 rather than a backtrace
|
|
71
|
+
- Thread-safe dep deduplication via class-level `_ran_tasks` Set + Mutex replaces Thor's `@_invocations`
|
|
72
|
+
- Removed `import` macro — task files use Ruby class reopening instead of modules
|
|
73
|
+
|
|
74
|
+
### Removed
|
|
75
|
+
|
|
76
|
+
- `SimpleFlow` dependency (replaced by `Dagwood`)
|
|
77
|
+
- `logger` gem workaround (was only needed for SimpleFlow on Ruby 4)
|
|
78
|
+
- `*.loki` glob fallback in `find_task_file` — only `.loki` is the auto-discovered entry point
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## [0.1.0] — 2026-05-28
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
|
|
86
|
+
- `Asgard::Base` — Thor subclass providing the task DSL
|
|
87
|
+
- `depends_on` — declare task dependencies; dependencies run at most once per invocation
|
|
88
|
+
- `var` — declare static or lazy-evaluated variables available to all tasks
|
|
89
|
+
- `import` — flat-merge a task module into the current class
|
|
90
|
+
- `dotenv` — load a `.env` file into the environment
|
|
91
|
+
- `sh` — run a shell command or multiline heredoc script; exits with the command's status on failure
|
|
92
|
+
- `shebang` — write a script body to a tempfile and execute it with a given interpreter (`:python3`, `:node`, `:ruby`, `:perl`, `:bash`, `:sh`, or any custom interpreter)
|
|
93
|
+
- `Asgard.find_task_files` — search current directory and ancestors for task files
|
|
94
|
+
- Task file resolution: `.loki` takes priority; falls back to all `*.loki` files sorted alphabetically
|
|
95
|
+
- `asgard` executable — finds task files, validates dependency graph, dispatches via Thor
|
|
96
|
+
- Circular dependency detection via `SimpleFlow::DependencyGraph` at startup
|
|
97
|
+
- 100% test coverage enforced via SimpleCov (95% minimum threshold)
|
|
98
|
+
- Quality task in `.loki` runs flog after tests
|
|
99
|
+
|
|
100
|
+
[Unreleased]: https://github.com/MadBomber/asgard/compare/v0.2.0...HEAD
|
|
101
|
+
[0.2.0]: https://github.com/MadBomber/asgard/compare/v0.1.2...v0.2.0
|
|
102
|
+
[0.1.2]: https://github.com/MadBomber/asgard/compare/v0.1.1...v0.1.2
|
|
103
|
+
[0.1.1]: https://github.com/MadBomber/asgard/compare/v0.1.0...v0.1.1
|
|
104
|
+
[0.1.0]: https://github.com/MadBomber/asgard/releases/tag/v0.1.0
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
## Circular Dependency Detection
|
|
149
|
+
|
|
150
|
+
Asgard validates the full dependency graph using [Dagwood](https://rubygems.org/gems/dagwood) before any task runs. A circular dependency produces a clean error and exits:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
class Tasks
|
|
154
|
+
depends_on :b
|
|
155
|
+
desc "Task A"; def a = puts "a"
|
|
156
|
+
|
|
157
|
+
depends_on :a
|
|
158
|
+
desc "Task B"; def b = puts "b"
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
asgard a
|
|
164
|
+
# asgard: circular dependency — TSort::Cyclic: ...
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
No backtrace is shown — just a single diagnostic line.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## depends_on Across Multiple Files
|
|
172
|
+
|
|
173
|
+
`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:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# build.loki
|
|
177
|
+
class Tasks
|
|
178
|
+
desc "Compile"
|
|
179
|
+
def build = sh "rake build"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# test.loki
|
|
183
|
+
class Tasks
|
|
184
|
+
depends_on :build # build.loki must be loaded first
|
|
185
|
+
desc "Test"
|
|
186
|
+
def test = sh "rake test"
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
When `--auto-load` is used, `*.loki` files are loaded alphabetically, so `build.loki` loads before `test.loki`. If you need to control load order, use explicit `require_relative` from `.loki`.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## depends_on Inside Subcommands
|
|
195
|
+
|
|
196
|
+
`depends_on` works within subcommand classes exactly as it does at the top level. Dependency scope is per-class:
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
class DBCommands < Tasks
|
|
200
|
+
desc "Run migrations"
|
|
201
|
+
def migrate = sh "rails db:migrate"
|
|
202
|
+
|
|
203
|
+
desc "Load seed data"
|
|
204
|
+
def seed = sh "rails db:seed"
|
|
205
|
+
|
|
206
|
+
depends_on :migrate, :seed
|
|
207
|
+
desc "Migrate then seed"
|
|
208
|
+
def reset = puts "Done."
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
class Tasks
|
|
212
|
+
desc "db SUBCOMMAND", "Manage the database"
|
|
213
|
+
subcommand "db", DBCommands
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
asgard db reset # migrate → seed → reset
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
See [Subcommands](subcommands.md) for the full guide.
|