asgard 0.2.0 → 0.3.1
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/.loki +13 -26
- data/.rubocop.yml +157 -0
- data/CHANGELOG.md +89 -11
- data/CLAUDE.md +19 -9
- data/README.md +161 -54
- data/docs/api.md +91 -15
- data/docs/changelog.md +12 -0
- data/docs/dependencies.md +25 -25
- data/docs/environment.md +30 -14
- data/docs/examples.md +3 -3
- data/docs/getting-started.md +5 -6
- data/docs/helpers.md +117 -9
- data/docs/index.md +6 -6
- data/docs/options.md +42 -8
- data/docs/shell.md +11 -11
- data/docs/subcommands.md +9 -9
- data/docs/task-files.md +266 -113
- data/docs/tasks.md +17 -15
- data/docs/variables.md +267 -51
- data/examples/.env +4 -0
- data/examples/.loki +24 -2
- data/examples/concurrent.loki +5 -5
- 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 +33 -24
- data/examples/subdir/.loki +12 -0
- data/examples/subdir/import_demo.loki +14 -0
- data/examples/subdir/import_up_demo.loki +18 -0
- data/gem_tasks.loki +35 -0
- data/lib/asgard/base.rb +158 -80
- data/lib/asgard/kernel_methods.rb +79 -0
- data/lib/asgard/shell.rb +8 -7
- data/lib/asgard/tasks.rb +12 -17
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +6 -18
- data/quality.loki +75 -0
- metadata +15 -5
- data/Rakefile +0 -22
data/docs/shell.md
CHANGED
|
@@ -14,10 +14,10 @@ Pass a single-line string to run it via `system`:
|
|
|
14
14
|
|
|
15
15
|
```ruby
|
|
16
16
|
class Tasks
|
|
17
|
-
desc "
|
|
17
|
+
desc "Compile the project"
|
|
18
18
|
def build = sh "rake build"
|
|
19
19
|
|
|
20
|
-
desc "
|
|
20
|
+
desc "Remove build artifacts"
|
|
21
21
|
def clean = sh "rm -rf dist/ tmp/"
|
|
22
22
|
end
|
|
23
23
|
```
|
|
@@ -34,7 +34,7 @@ Pass a multiline string (e.g., a heredoc) to run it as a single `bash -c` script
|
|
|
34
34
|
|
|
35
35
|
```ruby
|
|
36
36
|
class Tasks
|
|
37
|
-
desc "
|
|
37
|
+
desc "Bootstrap the development environment"
|
|
38
38
|
def setup
|
|
39
39
|
sh <<~SHELL
|
|
40
40
|
brew install redis postgresql
|
|
@@ -54,10 +54,10 @@ Pass `silent: true` to suppress the command echo. The command still runs and sti
|
|
|
54
54
|
|
|
55
55
|
```ruby
|
|
56
56
|
class Tasks
|
|
57
|
-
desc "
|
|
57
|
+
desc "Compile (quiet)"
|
|
58
58
|
def build = sh "rake build", silent: true
|
|
59
59
|
|
|
60
|
-
desc "
|
|
60
|
+
desc "Print environment info without noise"
|
|
61
61
|
def info
|
|
62
62
|
sh "printenv | grep APP_", silent: true
|
|
63
63
|
end
|
|
@@ -71,7 +71,7 @@ end
|
|
|
71
71
|
```ruby
|
|
72
72
|
class Tasks
|
|
73
73
|
depends_on :test
|
|
74
|
-
desc "
|
|
74
|
+
desc "Test then release"
|
|
75
75
|
def release
|
|
76
76
|
sh "bundle exec rake release"
|
|
77
77
|
# Never reached if rake release fails
|
|
@@ -90,7 +90,7 @@ end
|
|
|
90
90
|
|
|
91
91
|
```ruby
|
|
92
92
|
class Tasks
|
|
93
|
-
desc "
|
|
93
|
+
desc "Run Python data analysis"
|
|
94
94
|
def analyze
|
|
95
95
|
shebang :python3, <<~PYTHON
|
|
96
96
|
import json
|
|
@@ -105,7 +105,7 @@ end
|
|
|
105
105
|
|
|
106
106
|
```ruby
|
|
107
107
|
class Tasks
|
|
108
|
-
desc "
|
|
108
|
+
desc "Build frontend assets with esbuild"
|
|
109
109
|
def bundle_assets
|
|
110
110
|
shebang :node, <<~JS
|
|
111
111
|
const esbuild = require("esbuild")
|
|
@@ -123,7 +123,7 @@ end
|
|
|
123
123
|
|
|
124
124
|
```ruby
|
|
125
125
|
class Tasks
|
|
126
|
-
desc "
|
|
126
|
+
desc "Transform data with Ruby"
|
|
127
127
|
def transform
|
|
128
128
|
shebang :ruby, <<~RUBY
|
|
129
129
|
require "json"
|
|
@@ -138,7 +138,7 @@ end
|
|
|
138
138
|
|
|
139
139
|
```ruby
|
|
140
140
|
class Tasks
|
|
141
|
-
desc "
|
|
141
|
+
desc "Run a bash provisioning script"
|
|
142
142
|
def provision
|
|
143
143
|
shebang :bash, <<~BASH
|
|
144
144
|
set -euo pipefail
|
|
@@ -192,7 +192,7 @@ You can mix both in the same task:
|
|
|
192
192
|
|
|
193
193
|
```ruby
|
|
194
194
|
class Tasks
|
|
195
|
-
desc "
|
|
195
|
+
desc "Run a mixed shell + Python pipeline"
|
|
196
196
|
def pipeline
|
|
197
197
|
sh "bundle exec rake build"
|
|
198
198
|
|
data/docs/subcommands.md
CHANGED
|
@@ -10,10 +10,10 @@ Define a subcommand class that inherits from `Tasks`, then register it on the to
|
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
12
|
class DeployCommands < Tasks
|
|
13
|
-
desc "
|
|
13
|
+
desc "Deploy to staging"
|
|
14
14
|
def staging = sh "cap staging deploy"
|
|
15
15
|
|
|
16
|
-
desc "
|
|
16
|
+
desc "Deploy to production"
|
|
17
17
|
def production = sh "cap production deploy"
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -37,8 +37,8 @@ Inheriting from `Tasks` (rather than `Asgard::Base` or `Thor`) gives the subcomm
|
|
|
37
37
|
|
|
38
38
|
- `sh` and `shebang` shell helpers (from `Asgard::Shell`)
|
|
39
39
|
- `depends_on` for dependency declarations
|
|
40
|
-
- `var` for variables
|
|
41
40
|
- `dotenv` for environment loading
|
|
41
|
+
- `@@` class variables declared on `Tasks` (visible in all subclasses)
|
|
42
42
|
- The built-in `--debug` and `--verbose` class options
|
|
43
43
|
- The `debug?` and `verbose?` private predicates
|
|
44
44
|
- Any private helpers or `no_commands` methods defined on `Tasks`
|
|
@@ -54,14 +54,14 @@ Inheriting from `Tasks` (rather than `Asgard::Base` or `Thor`) gives the subcomm
|
|
|
54
54
|
|
|
55
55
|
```ruby
|
|
56
56
|
class DBCommands < Tasks
|
|
57
|
-
desc "
|
|
57
|
+
desc "Run pending migrations"
|
|
58
58
|
def migrate = sh "rails db:migrate"
|
|
59
59
|
|
|
60
|
-
desc "
|
|
60
|
+
desc "Load seed data"
|
|
61
61
|
def seed = sh "rails db:seed"
|
|
62
62
|
|
|
63
63
|
depends_on :migrate, :seed
|
|
64
|
-
desc "
|
|
64
|
+
desc "Migrate then seed"
|
|
65
65
|
def reset = puts "Done."
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -94,13 +94,13 @@ class ServerCommands < Tasks
|
|
|
94
94
|
sh "puma -p #{port} #{flags.join(' ')}"
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
desc "
|
|
97
|
+
desc "Stop the running server"
|
|
98
98
|
option :force, aliases: "-f", type: :boolean, default: false, desc: "Force-kill without draining"
|
|
99
99
|
def stop
|
|
100
100
|
options[:force] ? sh "pkill -9 puma" : sh "pumactl stop"
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
desc "
|
|
103
|
+
desc "Show server status"
|
|
104
104
|
def status = sh "pumactl stats"
|
|
105
105
|
|
|
106
106
|
depends_on :stop, :start
|
|
@@ -176,6 +176,6 @@ myproject/
|
|
|
176
176
|
db_subcommands.loki ← defines DBCommands
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
Because siblings loaded via `import "*.loki"` execute before `.loki`'s own class body, both `DBCommands` and `ServerCommands` are defined by the time `.loki` runs its `subcommand` calls.
|
|
180
180
|
|
|
181
181
|
See [`examples/server_subcommands.loki`](examples.md#server-subcommands) and [`examples/db_subcommands.loki`](examples.md#db-subcommands) for complete working examples.
|
data/docs/task-files.md
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# Task Files
|
|
2
2
|
|
|
3
|
-
Asgard uses a convention-based file discovery system. A hidden `.loki` file marks the project root
|
|
3
|
+
Asgard uses a convention-based file discovery system. A hidden `.loki` file marks the project root. Everything else — loading sibling files, shared task libraries, monorepo-wide tasks — is controlled explicitly from inside your `.loki` file using the `import` and `import_up` Kernel methods.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
## The `.loki` Root Marker
|
|
8
8
|
|
|
9
|
-
When you run `asgard`, it searches for a `.loki` file starting in the current working directory and walking upward through parent directories until it finds one or reaches the filesystem root. The first `.loki` file found marks the project root
|
|
10
|
-
|
|
11
|
-
This means you can run `asgard` from any subdirectory of your project and it will find your tasks:
|
|
9
|
+
When you run `asgard`, it searches for a `.loki` file starting in the current working directory and walking upward through parent directories until it finds one or reaches the filesystem root. The first `.loki` file found marks the project root and is the only file Asgard loads automatically.
|
|
12
10
|
|
|
13
11
|
```
|
|
14
12
|
myproject/
|
|
@@ -18,143 +16,223 @@ myproject/
|
|
|
18
16
|
# asgard still works from here
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
The `.loki` file can be completely empty. Its presence alone is sufficient to mark the project root. If it is empty and you have `*.loki` files, you must pass `--auto-load` when running `asgard` — otherwise Asgard has nothing to do.
|
|
19
|
+
The `.loki` file can be completely empty — its presence alone marks the project root. It can also contain task definitions, `import` calls, or any valid Ruby.
|
|
23
20
|
|
|
24
21
|
---
|
|
25
22
|
|
|
26
|
-
## Loading
|
|
23
|
+
## Loading Files with `import`
|
|
24
|
+
|
|
25
|
+
`import` is a Kernel method available everywhere in Ruby — at the top level of `.loki` files, inside class bodies, and inside task method bodies. It loads `.loki` files with `require`-like idempotency: a file is loaded at most once per process, no matter how many times `import` is called with the same path.
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
### Single file by absolute path
|
|
29
28
|
|
|
30
|
-
```
|
|
31
|
-
|
|
29
|
+
```ruby
|
|
30
|
+
import "/home/shared/gem_tasks.loki"
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
### Single file by relative path
|
|
35
34
|
|
|
36
|
-
**
|
|
35
|
+
Relative paths are resolved relative to the **caller's file location**, like `require_relative`:
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
```ruby
|
|
38
|
+
# .loki — relative to this file's directory
|
|
39
|
+
import "build.loki"
|
|
40
|
+
import "../shared/gem_tasks.loki"
|
|
41
|
+
import "tasks/ci.loki"
|
|
42
|
+
```
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
### All files in the same directory (glob)
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
```ruby
|
|
47
|
+
import "*.loki" # all *.loki files in the same directory as the calling file
|
|
48
|
+
```
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
`*.loki` never matches `.loki` (the dotfile entry point) — Ruby's `Dir.glob` excludes dotfiles from `*` patterns by default.
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
### All files recursively (recursive glob)
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
| `desc` metadata | Thor registers the new usage/description string, discarding the old one |
|
|
53
|
-
| `depends_on` stages | `method_added` captures the pending deps for the new definition; the earlier dep chain is replaced |
|
|
54
|
+
```ruby
|
|
55
|
+
import "**/*.loki" # every .loki file in this directory and all subdirectories
|
|
56
|
+
```
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
### Specific named files
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
```ruby
|
|
61
|
+
import "gem_tasks.loki"
|
|
62
|
+
import "ci_tasks.loki"
|
|
63
|
+
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
### Combining patterns
|
|
61
66
|
|
|
62
67
|
```ruby
|
|
63
|
-
#
|
|
68
|
+
# .loki
|
|
69
|
+
import "*.loki" # load all siblings
|
|
70
|
+
import "../shared/*.loki" # load a parent-level shared library
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Typical `.loki` entry point
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# .loki
|
|
77
|
+
import "*.loki" # load all sibling task files
|
|
78
|
+
|
|
64
79
|
class Tasks
|
|
65
|
-
|
|
66
|
-
def build
|
|
67
|
-
sh "rake build"
|
|
68
|
-
end
|
|
80
|
+
# any top-level task definitions or overrides
|
|
69
81
|
end
|
|
70
82
|
```
|
|
71
83
|
|
|
84
|
+
### Return value
|
|
85
|
+
|
|
86
|
+
`import` returns `true` if at least one file was newly loaded, `false` if all files were already loaded or no glob pattern matched any file. If a specific (non-glob) file does not exist, `import` raises `LoadError`.
|
|
87
|
+
|
|
72
88
|
```ruby
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# Preserve the original under a private name before overwriting it.
|
|
76
|
-
no_commands { alias_method :_build_original, :build }
|
|
89
|
+
import("gem_tasks.loki") ? "loaded now" : "already loaded"
|
|
90
|
+
```
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Finding Files with `loki_up`
|
|
95
|
+
|
|
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
|
+
|
|
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
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
loki_up # finds .loki (the project root marker)
|
|
102
|
+
loki_up("gem_tasks.loki") # finds gem_tasks.loki in CWD or any ancestor
|
|
103
|
+
loki_up(".env") # finds the nearest .env file up the tree
|
|
104
|
+
loki_up("VERSION") # finds a VERSION file in CWD or any ancestor
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `loki_up` when you need the path for other purposes, or to check whether a file exists before deciding to load it:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
if (path = loki_up("gem_tasks.loki"))
|
|
111
|
+
import path
|
|
83
112
|
end
|
|
113
|
+
|
|
114
|
+
# Pass the located .env to dotenv — works from any subdirectory
|
|
115
|
+
dotenv loki_up(".env") || ".env"
|
|
84
116
|
```
|
|
85
117
|
|
|
86
|
-
`
|
|
118
|
+
`loki_up` accepts exact filenames only. Glob patterns are not expanded by `loki_up` — use `import_up` for glob-aware ancestor search.
|
|
87
119
|
|
|
88
|
-
|
|
89
|
-
The `_` prefix on the alias name (`_build_original`) follows Asgard's convention for non-user-facing methods and reinforces that it is an implementation detail, not a task to be invoked directly.
|
|
120
|
+
---
|
|
90
121
|
|
|
91
|
-
|
|
92
|
-
Using `alias_method` to bolt post-task behaviour onto an existing task is a code smell. It is fragile (load-order dependent), obscures intent, and makes the dependency chain invisible to Asgard's cycle-detection and deduplication logic.
|
|
122
|
+
## Loading Files Found up the Tree with `import_up`
|
|
93
123
|
|
|
94
|
-
|
|
124
|
+
`import_up(name = ".loki")` combines `loki_up` and `import` into a single call. It finds the file (or files) up the ancestor chain and loads them.
|
|
95
125
|
|
|
96
|
-
|
|
97
|
-
# build.loki
|
|
98
|
-
class Tasks
|
|
99
|
-
desc "build", "Compile the project"
|
|
100
|
-
def build = sh "rake build"
|
|
126
|
+
### Exact filename
|
|
101
127
|
|
|
102
|
-
|
|
103
|
-
|
|
128
|
+
```ruby
|
|
129
|
+
import_up "gem_tasks.loki"
|
|
130
|
+
```
|
|
104
131
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
Walks up from `Dir.pwd` until it finds `gem_tasks.loki`, then loads it. Returns `false` if not found anywhere.
|
|
133
|
+
|
|
134
|
+
### Glob pattern
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
import_up "*.loki"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Walks up from `Dir.pwd` and stops at the **first ancestor directory** that contains any `*.loki` files, loading all of them. It does not aggregate matches from multiple levels — it loads only the nearest match, then stops.
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
~/sandbox/
|
|
144
|
+
gem_tasks.loki ← loaded by import_up("*.loki") from ~/sandbox/myproject/sub/
|
|
145
|
+
ci_tasks.loki ← also loaded — same directory as the first match
|
|
146
|
+
myproject/
|
|
147
|
+
.loki
|
|
148
|
+
sub/
|
|
149
|
+
# Dir.pwd here; import_up("*.loki") finds ~/sandbox/*.loki files
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Return value
|
|
153
|
+
|
|
154
|
+
Returns `true` if any file was newly loaded, `false` if the file was not found or was already loaded.
|
|
155
|
+
|
|
156
|
+
### Conditional load
|
|
157
|
+
|
|
158
|
+
Since `import_up` returns `false` when a file is not found (rather than raising), it composes naturally with `||`:
|
|
110
159
|
|
|
111
|
-
|
|
160
|
+
```ruby
|
|
161
|
+
import_up("project_tasks.loki") || import_up("gem_tasks.loki")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Idempotency
|
|
167
|
+
|
|
168
|
+
Both `import` and `import_up` track loaded files in Ruby's `$LOADED_FEATURES`. A second call with the same path is a no-op and returns `false`. This means:
|
|
169
|
+
|
|
170
|
+
- You can call `import "*.loki"` from both `.loki` and a shared task file without double-loading.
|
|
171
|
+
- `import_up("gem_tasks.loki")` from two different projects in the same process each load their nearest match once.
|
|
172
|
+
- Swapping `require` for `import` in a `.loki` file gives the same once-per-process guarantee.
|
|
112
173
|
|
|
113
174
|
---
|
|
114
175
|
|
|
115
|
-
##
|
|
176
|
+
## Verbose and Debug Feedback
|
|
116
177
|
|
|
117
|
-
|
|
178
|
+
`import` and `import_up` emit diagnostic messages to stderr when the `verbose?` or `debug?` flags are active (set via `--verbose` or `--debug` on the CLI, or by setting `$VERBOSE`/`$DEBUG` directly):
|
|
179
|
+
|
|
180
|
+
| Flag | `import` output | `import_up` output |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `--verbose` | Prints each file path as it is loaded | Prints `name → /full/path` when found |
|
|
183
|
+
| `--debug` | Same as verbose, plus prints a skip message for already-loaded files | Same as verbose, plus prints `name not found` when the search comes up empty |
|
|
118
184
|
|
|
119
185
|
```
|
|
120
|
-
|
|
121
|
-
|
|
186
|
+
$ asgard --verbose build
|
|
187
|
+
import: /home/user/myproject/build.loki
|
|
188
|
+
import: /home/user/myproject/test.loki
|
|
122
189
|
```
|
|
123
190
|
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Loading Patterns
|
|
194
|
+
|
|
195
|
+
### Single-file project
|
|
196
|
+
|
|
197
|
+
All tasks in `.loki`, nothing else:
|
|
198
|
+
|
|
124
199
|
```ruby
|
|
125
200
|
# .loki
|
|
126
201
|
class Tasks
|
|
127
|
-
|
|
202
|
+
@@app ||= "myapp".freeze
|
|
128
203
|
|
|
129
|
-
desc "
|
|
204
|
+
desc "Compile the project"
|
|
130
205
|
def build = sh "rake build"
|
|
131
206
|
|
|
132
|
-
desc "
|
|
207
|
+
desc "Run the test suite"
|
|
133
208
|
def test = sh "rake test"
|
|
134
209
|
|
|
135
|
-
desc "
|
|
136
|
-
def release = sh "gem push pkg/#{app}-*.gem"
|
|
210
|
+
desc "Build and push the gem"
|
|
211
|
+
def release = sh "gem push pkg/#{@@app}-*.gem"
|
|
137
212
|
end
|
|
138
213
|
```
|
|
139
214
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
## Multi-File Layout
|
|
215
|
+
### Multi-file project
|
|
143
216
|
|
|
144
|
-
Split tasks across files by concern
|
|
217
|
+
Split tasks across files by concern. Load them all from `.loki` with a glob:
|
|
145
218
|
|
|
146
219
|
```
|
|
147
220
|
myproject/
|
|
148
|
-
.loki ← entry point
|
|
149
|
-
build.loki ← build
|
|
150
|
-
deploy.loki ←
|
|
221
|
+
.loki ← entry point; imports siblings
|
|
222
|
+
build.loki ← build tasks
|
|
223
|
+
deploy.loki ← deploy tasks
|
|
151
224
|
test.loki ← test tasks
|
|
152
225
|
```
|
|
153
226
|
|
|
227
|
+
```ruby
|
|
228
|
+
# .loki
|
|
229
|
+
import "*.loki"
|
|
230
|
+
```
|
|
231
|
+
|
|
154
232
|
```ruby
|
|
155
233
|
# build.loki
|
|
156
234
|
class Tasks
|
|
157
|
-
desc "
|
|
235
|
+
desc "Compile the project"
|
|
158
236
|
def build = sh "rake build"
|
|
159
237
|
end
|
|
160
238
|
```
|
|
@@ -163,7 +241,7 @@ end
|
|
|
163
241
|
# test.loki
|
|
164
242
|
class Tasks
|
|
165
243
|
depends_on :build
|
|
166
|
-
desc "
|
|
244
|
+
desc "Run the test suite"
|
|
167
245
|
def test = sh "bundle exec rake test"
|
|
168
246
|
end
|
|
169
247
|
```
|
|
@@ -172,83 +250,158 @@ end
|
|
|
172
250
|
# deploy.loki
|
|
173
251
|
class Tasks
|
|
174
252
|
depends_on :test
|
|
175
|
-
desc "
|
|
253
|
+
desc "Deploy to production"
|
|
176
254
|
def deploy = sh "cap production deploy"
|
|
177
255
|
end
|
|
178
256
|
```
|
|
179
257
|
|
|
180
|
-
|
|
181
|
-
# .loki — can be empty, or can register subcommands, add top-level vars, etc.
|
|
182
|
-
```
|
|
258
|
+
Files loaded via glob are sorted alphabetically by `Dir.glob`, so `build.loki` loads before `test.loki`. Tasks defined in earlier files are available to later files via `depends_on`.
|
|
183
259
|
|
|
184
|
-
|
|
260
|
+
### Controlled load order
|
|
185
261
|
|
|
186
|
-
|
|
187
|
-
When `--auto-load` is used, `*.loki` files are sorted alphabetically, so `build.loki` loads before `test.loki`, which means `depends_on :build` in `test.loki` correctly references a task that already exists.
|
|
262
|
+
When alphabetical order does not match your dependency order, import explicitly:
|
|
188
263
|
|
|
189
|
-
|
|
264
|
+
```ruby
|
|
265
|
+
# .loki
|
|
266
|
+
import "infra.loki" # must be first
|
|
267
|
+
import "build.loki" # depends on infra
|
|
268
|
+
import "deploy.loki" # depends on build
|
|
269
|
+
```
|
|
190
270
|
|
|
191
|
-
|
|
271
|
+
### Shared task library in a monorepo
|
|
192
272
|
|
|
193
|
-
|
|
273
|
+
Place shared tasks in a parent directory and load them from any sub-project:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
~/sandbox/
|
|
277
|
+
gem_tasks.loki ← shared: build, install, release tasks for any gem
|
|
278
|
+
myproject/
|
|
279
|
+
.loki ← loads gem_tasks.loki via import_up
|
|
280
|
+
other_project/
|
|
281
|
+
.loki ← also loads gem_tasks.loki via import_up
|
|
282
|
+
```
|
|
194
283
|
|
|
195
284
|
```ruby
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
require_relative "ci.loki"
|
|
285
|
+
# myproject/.loki
|
|
286
|
+
import_up "gem_tasks.loki" # finds ~/sandbox/gem_tasks.loki
|
|
199
287
|
|
|
200
288
|
class Tasks
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
desc "full-ci", "Complete CI run"
|
|
204
|
-
def full_ci = sh "echo 'full CI complete'"
|
|
289
|
+
# project-specific overrides here
|
|
205
290
|
end
|
|
206
291
|
```
|
|
207
292
|
|
|
208
|
-
|
|
293
|
+
### Conditional shared library
|
|
209
294
|
|
|
210
|
-
|
|
211
|
-
|
|
295
|
+
```ruby
|
|
296
|
+
# .loki
|
|
297
|
+
import_up("ci_tasks.loki") || import_up("gem_tasks.loki")
|
|
298
|
+
```
|
|
212
299
|
|
|
213
|
-
|
|
300
|
+
Loads `ci_tasks.loki` if found up the tree, otherwise falls back to `gem_tasks.loki`.
|
|
214
301
|
|
|
215
|
-
|
|
302
|
+
### Subcommand classes across files
|
|
216
303
|
|
|
217
|
-
|
|
304
|
+
Define subcommand classes in separate files and register them in `.loki`:
|
|
218
305
|
|
|
219
306
|
```
|
|
220
307
|
myproject/
|
|
221
|
-
.loki
|
|
222
|
-
|
|
223
|
-
|
|
308
|
+
.loki
|
|
309
|
+
db.loki
|
|
310
|
+
server.loki
|
|
224
311
|
```
|
|
225
312
|
|
|
226
313
|
```ruby
|
|
227
|
-
#
|
|
314
|
+
# db.loki
|
|
228
315
|
class DBCommands < Tasks
|
|
229
|
-
desc "
|
|
316
|
+
desc "Run migrations"
|
|
230
317
|
def migrate = sh "rails db:migrate"
|
|
231
318
|
end
|
|
319
|
+
```
|
|
232
320
|
|
|
233
|
-
|
|
321
|
+
```ruby
|
|
322
|
+
# server.loki
|
|
234
323
|
class ServerCommands < Tasks
|
|
235
|
-
desc "
|
|
324
|
+
desc "Start the server"
|
|
236
325
|
def start = sh "rails server"
|
|
237
326
|
end
|
|
327
|
+
```
|
|
238
328
|
|
|
329
|
+
```ruby
|
|
239
330
|
# .loki
|
|
331
|
+
import "*.loki" # db.loki and server.loki load first
|
|
332
|
+
|
|
240
333
|
class Tasks
|
|
241
334
|
desc "db SUBCOMMAND", "Manage the database"; subcommand "db", DBCommands
|
|
242
335
|
desc "server SUBCOMMAND", "Manage the server"; subcommand "server", ServerCommands
|
|
243
336
|
end
|
|
244
337
|
```
|
|
245
338
|
|
|
339
|
+
Subcommand classes are available in `.loki` because siblings loaded via `import "*.loki"` execute before `.loki`'s own class body.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Task Name Overloading
|
|
344
|
+
|
|
345
|
+
Because all `*.loki` files reopen the same `class Tasks`, two files can define a method with the same name. Ruby's class reopening semantics apply: the last definition loaded wins, silently replacing the earlier one.
|
|
346
|
+
|
|
347
|
+
Three things are overwritten when a task name is reused:
|
|
348
|
+
|
|
349
|
+
| What | Effect |
|
|
350
|
+
|---|---|
|
|
351
|
+
| `def method_name` | The Ruby method body — the earlier implementation is gone |
|
|
352
|
+
| `desc` metadata | Thor registers the new usage/description string, discarding the old one |
|
|
353
|
+
| `depends_on` stages | `method_added` captures the pending deps for the new definition; the earlier dep chain is replaced |
|
|
354
|
+
|
|
355
|
+
**Accidental overloading** is a silent bug. Keep task names unique across files.
|
|
356
|
+
|
|
357
|
+
!!! warning
|
|
358
|
+
There is no runtime error when a task is overloaded. If a task is not behaving as expected, check whether another `.loki` file defines the same method name and loads after it.
|
|
359
|
+
|
|
360
|
+
**Intentional overloading** lets you extend a task defined in an earlier file using `alias_method`:
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# build.loki (loaded first)
|
|
364
|
+
class Tasks
|
|
365
|
+
desc "Compile the project"
|
|
366
|
+
def build = sh "rake build"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# postbuild.loki (loaded after build.loki, alphabetically)
|
|
370
|
+
class Tasks
|
|
371
|
+
no_commands { alias_method :_build_original, :build }
|
|
372
|
+
|
|
373
|
+
desc "Compile the project and copy assets"
|
|
374
|
+
def build
|
|
375
|
+
_build_original
|
|
376
|
+
sh "cp -r dist/ public/"
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
!!! warning "Prefer `depends_on` over intentional overloading"
|
|
382
|
+
Using `alias_method` to bolt post-task behaviour onto an existing task is fragile and load-order dependent. The idiomatic alternative is `depends_on`:
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
class Tasks
|
|
386
|
+
desc "Compile the project"
|
|
387
|
+
def build = sh "rake build"
|
|
388
|
+
|
|
389
|
+
desc "Copy build output to public/"
|
|
390
|
+
def copy_assets = sh "cp -r dist/ public/"
|
|
391
|
+
|
|
392
|
+
depends_on :build, :copy_assets
|
|
393
|
+
desc "Compile and copy assets"
|
|
394
|
+
def build_all; end
|
|
395
|
+
end
|
|
396
|
+
```
|
|
397
|
+
|
|
246
398
|
---
|
|
247
399
|
|
|
248
400
|
## Summary of Loading Rules
|
|
249
401
|
|
|
250
|
-
|
|
|
251
|
-
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
| `
|
|
402
|
+
| Method | Finds? | Loads? | Glob? | Ancestor search? |
|
|
403
|
+
|---|---|---|---|---|
|
|
404
|
+
| `loki_up(name)` | Yes (`Pathname`) | No | No | Yes |
|
|
405
|
+
| `import(path)` | No | Yes | Yes | No |
|
|
406
|
+
| `import_up(name)` | Yes | Yes | Yes | Yes |
|
|
407
|
+
| Asgard's `run!` | Yes | `.loki` only | No | Yes |
|