asgard 0.2.0 → 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/.loki +7 -9
- data/.rubocop.yml +157 -0
- data/CHANGELOG.md +49 -11
- data/CLAUDE.md +19 -9
- data/README.md +78 -53
- data/Rakefile +83 -4
- data/docs/api.md +86 -13
- data/docs/changelog.md +4 -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 +34 -10
- data/docs/index.md +6 -6
- data/docs/options.md +2 -2
- 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 +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 +125 -83
- data/lib/asgard/kernel_methods.rb +77 -0
- data/lib/asgard/shell.rb +8 -7
- data/lib/asgard/tasks.rb +0 -11
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +2 -18
- metadata +13 -4
data/README.md
CHANGED
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
- <strong>Task Dependencies</strong> — sequential, parallel, and mixed dependency graphs via <code>depends_on</code><br>
|
|
18
18
|
- <strong>Concurrent Execution</strong> — parallel task groups run in native Ruby threads<br>
|
|
19
19
|
- <strong>Subcommands</strong> — group related tasks under a named namespace<br>
|
|
20
|
-
- <strong>Variables</strong> —
|
|
20
|
+
- <strong>Variables</strong> — shared configuration via Ruby class variables (<code>@@name</code>), visible across all tasks and subcommands<br>
|
|
21
21
|
- <strong>Shell Helpers</strong> — <code>sh</code> for any shell command or heredoc; <code>shebang</code> for polyglot scripts<br>
|
|
22
22
|
- <strong>Dotenv Support</strong> — load <code>.env</code> files into the environment with <code>dotenv</code><br>
|
|
23
23
|
- <strong>Auto-Discovery</strong> — <code>.loki</code> root marker searched from CWD upward through parent directories<br>
|
|
24
|
-
- <strong>Multi-File Tasks</strong> — split tasks across <code>*.loki</code> files, loaded
|
|
25
|
-
- <strong>Built-in Flags</strong> — <code>--
|
|
24
|
+
- <strong>Multi-File Tasks</strong> — split tasks across <code>*.loki</code> files, loaded via <code>import</code> from your <code>.loki</code><br>
|
|
25
|
+
- <strong>Built-in Flags</strong> — <code>--debug</code> and <code>--verbose</code> available on every task; <code>--version</code> at the top level<br>
|
|
26
26
|
</td>
|
|
27
27
|
</tr>
|
|
28
28
|
</table>
|
|
@@ -51,8 +51,8 @@ Every `.loki` file defines tasks as methods inside `class Tasks`. The `Tasks` cl
|
|
|
51
51
|
|
|
52
52
|
```ruby
|
|
53
53
|
class Tasks
|
|
54
|
-
desc "
|
|
55
|
-
def hello =
|
|
54
|
+
desc "Say hello"
|
|
55
|
+
def hello = puts "Hello, World!"
|
|
56
56
|
end
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -67,7 +67,7 @@ Declare positional parameters directly in the method signature. Document them in
|
|
|
67
67
|
```ruby
|
|
68
68
|
class Tasks
|
|
69
69
|
desc "hello NAME", "Say hello to NAME"
|
|
70
|
-
def hello(name = "World") =
|
|
70
|
+
def hello(name = "World") = puts "Hello, #{name}!"
|
|
71
71
|
end
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -88,7 +88,7 @@ class Tasks
|
|
|
88
88
|
desc: "Name to greet"
|
|
89
89
|
|
|
90
90
|
desc "hello NAME", "Say hello to NAME"
|
|
91
|
-
def hello =
|
|
91
|
+
def hello = puts "Hello, #{name}!"
|
|
92
92
|
end
|
|
93
93
|
```
|
|
94
94
|
|
|
@@ -103,7 +103,7 @@ class Tasks
|
|
|
103
103
|
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
|
|
104
104
|
def hello(name = "World")
|
|
105
105
|
message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
|
|
106
|
-
options[:count].times {
|
|
106
|
+
options[:count].times { puts message }
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
```
|
|
@@ -128,7 +128,7 @@ class Tasks
|
|
|
128
128
|
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
|
|
129
129
|
def hello(name = "World")
|
|
130
130
|
message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
|
|
131
|
-
options[:count].times {
|
|
131
|
+
options[:count].times { puts message }
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
134
|
```
|
|
@@ -139,7 +139,7 @@ end
|
|
|
139
139
|
|
|
140
140
|
`depends_on` declares what must run before a task. Each dependency runs at most once per `asgard` invocation regardless of how many tasks declare it. Circular dependencies are caught at startup.
|
|
141
141
|
|
|
142
|
-
`desc` and `depends_on` are independent — either can come first, both must appear before `def`.
|
|
142
|
+
`desc` and `depends_on` are independent — either can come first, both must appear before `def`.
|
|
143
143
|
|
|
144
144
|
### Sequential dependencies
|
|
145
145
|
|
|
@@ -147,15 +147,15 @@ Bare symbols run one after another in the order declared:
|
|
|
147
147
|
|
|
148
148
|
```ruby
|
|
149
149
|
class Tasks
|
|
150
|
-
desc "
|
|
150
|
+
desc "Compile the project"
|
|
151
151
|
def build = sh "rake build"
|
|
152
152
|
|
|
153
153
|
depends_on :build
|
|
154
|
-
desc "
|
|
154
|
+
desc "Run the test suite"
|
|
155
155
|
def test = sh "rake test"
|
|
156
156
|
|
|
157
157
|
depends_on :test
|
|
158
|
-
desc "
|
|
158
|
+
desc "Publish the gem"
|
|
159
159
|
def release = sh "bundle exec rake release"
|
|
160
160
|
end
|
|
161
161
|
```
|
|
@@ -170,15 +170,15 @@ Wrap symbols in an array to declare they can run concurrently. Asgard waits for
|
|
|
170
170
|
|
|
171
171
|
```ruby
|
|
172
172
|
class Tasks
|
|
173
|
-
desc "
|
|
173
|
+
desc "Check code style"
|
|
174
174
|
def lint = sh "bundle exec rubocop"
|
|
175
175
|
|
|
176
|
-
desc "
|
|
176
|
+
desc "Run type checks"
|
|
177
177
|
def typecheck = sh "bundle exec srb tc"
|
|
178
178
|
|
|
179
179
|
# lint and typecheck run in parallel, test waits for both
|
|
180
180
|
depends_on [:lint, :typecheck]
|
|
181
|
-
desc "
|
|
181
|
+
desc "Run the test suite"
|
|
182
182
|
def test = sh "bundle exec rake test"
|
|
183
183
|
end
|
|
184
184
|
```
|
|
@@ -193,16 +193,16 @@ Mix bare symbols (sequential) and arrays (parallel) in a single `depends_on` cal
|
|
|
193
193
|
|
|
194
194
|
```ruby
|
|
195
195
|
class Tasks
|
|
196
|
-
desc "
|
|
197
|
-
desc "
|
|
198
|
-
desc "
|
|
199
|
-
desc "
|
|
200
|
-
desc "
|
|
196
|
+
desc "Install dependencies"; def setup = sh "bundle install"
|
|
197
|
+
desc "Check code style"; def lint = sh "bundle exec rubocop"
|
|
198
|
+
desc "Compile assets"; def build = sh "rake assets:precompile"
|
|
199
|
+
desc "Run tests"; def test = sh "bundle exec rake test"
|
|
200
|
+
desc "Post to Slack"; def notify = sh "curl $SLACK_WEBHOOK -d '{\"text\":\"done\"}'"
|
|
201
201
|
|
|
202
202
|
# setup first, then lint+build in parallel, then test, then notify
|
|
203
203
|
depends_on :setup, [:lint, :build], :test, :notify
|
|
204
|
-
desc "
|
|
205
|
-
def ci =
|
|
204
|
+
desc "Full CI pipeline"
|
|
205
|
+
def ci = puts "CI complete"
|
|
206
206
|
end
|
|
207
207
|
```
|
|
208
208
|
|
|
@@ -224,18 +224,24 @@ asgard ci executes:
|
|
|
224
224
|
|
|
225
225
|
## Variables
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
Shared configuration values are declared as Ruby class variables (`@@name`) at the top of the class body. Use `||=` so the first declaration wins when multiple `.loki` files reopen `Tasks`, and `.freeze` to prevent mutation:
|
|
228
228
|
|
|
229
229
|
```ruby
|
|
230
230
|
class Tasks
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
@@app ||= "myapp".freeze
|
|
232
|
+
@@max_jobs ||= 4
|
|
233
233
|
|
|
234
|
-
desc "
|
|
235
|
-
def tag = sh "git tag #{app}-#{version}"
|
|
234
|
+
desc "Create a release tag"
|
|
235
|
+
def tag = sh "git tag #{@@app}-#{version}"
|
|
236
|
+
|
|
237
|
+
private
|
|
238
|
+
|
|
239
|
+
def version = `git describe --tags`.strip
|
|
236
240
|
end
|
|
237
241
|
```
|
|
238
242
|
|
|
243
|
+
Class variables are visible in all task instance methods and in subcommand subclasses — unlike class instance variables (`@name`), which are not accessible inside instance methods.
|
|
244
|
+
|
|
239
245
|
---
|
|
240
246
|
|
|
241
247
|
## Helper methods
|
|
@@ -244,13 +250,13 @@ Private methods are callable from any task in the same class but are never regis
|
|
|
244
250
|
|
|
245
251
|
```ruby
|
|
246
252
|
class Tasks
|
|
247
|
-
desc "
|
|
253
|
+
desc "Compile and package"
|
|
248
254
|
def build
|
|
249
255
|
compile("src")
|
|
250
256
|
package(version)
|
|
251
257
|
end
|
|
252
258
|
|
|
253
|
-
desc "
|
|
259
|
+
desc "Build and publish"
|
|
254
260
|
def release
|
|
255
261
|
build
|
|
256
262
|
sh "gem push pkg/myapp-#{version}.gem"
|
|
@@ -286,7 +292,7 @@ require_relative "shared/helpers"
|
|
|
286
292
|
class Tasks
|
|
287
293
|
include BuildHelpers
|
|
288
294
|
|
|
289
|
-
desc "
|
|
295
|
+
desc "Compile the project"
|
|
290
296
|
def build = compile("src")
|
|
291
297
|
end
|
|
292
298
|
```
|
|
@@ -320,7 +326,7 @@ end
|
|
|
320
326
|
|
|
321
327
|
```ruby
|
|
322
328
|
class Tasks
|
|
323
|
-
desc "
|
|
329
|
+
desc "Bootstrap the development environment"
|
|
324
330
|
def setup
|
|
325
331
|
sh <<~SHELL
|
|
326
332
|
brew install redis postgresql
|
|
@@ -330,7 +336,7 @@ class Tasks
|
|
|
330
336
|
SHELL
|
|
331
337
|
end
|
|
332
338
|
|
|
333
|
-
desc "
|
|
339
|
+
desc "Run Python data analysis"
|
|
334
340
|
def analyze
|
|
335
341
|
shebang :python3, <<~PYTHON
|
|
336
342
|
import json
|
|
@@ -339,7 +345,7 @@ class Tasks
|
|
|
339
345
|
PYTHON
|
|
340
346
|
end
|
|
341
347
|
|
|
342
|
-
desc "
|
|
348
|
+
desc "Build frontend assets with esbuild"
|
|
343
349
|
def bundle_assets
|
|
344
350
|
shebang :node, <<~JS
|
|
345
351
|
const esbuild = require("esbuild")
|
|
@@ -365,18 +371,31 @@ PY
|
|
|
365
371
|
|
|
366
372
|
## Environment variables
|
|
367
373
|
|
|
368
|
-
`dotenv` loads a `.env` file into the environment before tasks run:
|
|
374
|
+
`dotenv` loads a `.env` file into the environment before tasks run. Use the `env` Kernel method to read environment variables inside task bodies — it accepts a symbol or string and upcases the key automatically:
|
|
369
375
|
|
|
370
376
|
```ruby
|
|
371
377
|
class Tasks
|
|
372
378
|
dotenv # loads .env
|
|
373
379
|
dotenv ".env.local" # or a specific file
|
|
374
380
|
|
|
375
|
-
desc "
|
|
376
|
-
def
|
|
381
|
+
desc "Start the server"
|
|
382
|
+
def start
|
|
383
|
+
sh "puma -p #{env(:port, '3000')} -e #{env(:rack_env, 'development')}"
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
desc "Deploy the app"
|
|
387
|
+
def deploy
|
|
388
|
+
sh "cap #{env(:deploy_target)} deploy" # raises KeyError if DEPLOY_TARGET is unset
|
|
389
|
+
end
|
|
377
390
|
end
|
|
378
391
|
```
|
|
379
392
|
|
|
393
|
+
| Call | Behaviour |
|
|
394
|
+
|------|-----------|
|
|
395
|
+
| `env(:port, "3000")` | Returns `"3000"` when `PORT` is unset |
|
|
396
|
+
| `env(:api_key)` | Raises `KeyError` when `API_KEY` is missing |
|
|
397
|
+
| `env("DATABASE_URL")` | String name works too — always upcased |
|
|
398
|
+
|
|
380
399
|
---
|
|
381
400
|
|
|
382
401
|
## Command aliases
|
|
@@ -389,7 +408,7 @@ class Tasks
|
|
|
389
408
|
map "--v" => "version"
|
|
390
409
|
map "t" => "test"
|
|
391
410
|
|
|
392
|
-
desc "
|
|
411
|
+
desc "Print the version"
|
|
393
412
|
def version = puts Asgard::VERSION
|
|
394
413
|
end
|
|
395
414
|
```
|
|
@@ -402,10 +421,10 @@ Group related tasks under a common name using Thor's `subcommand` method. Define
|
|
|
402
421
|
|
|
403
422
|
```ruby
|
|
404
423
|
class DeployCommands < Tasks
|
|
405
|
-
desc "
|
|
424
|
+
desc "Deploy to staging"
|
|
406
425
|
def staging = sh "cap staging deploy"
|
|
407
426
|
|
|
408
|
-
desc "
|
|
427
|
+
desc "Deploy to production"
|
|
409
428
|
def production = sh "cap production deploy"
|
|
410
429
|
end
|
|
411
430
|
|
|
@@ -421,20 +440,20 @@ asgard deploy staging
|
|
|
421
440
|
asgard deploy production
|
|
422
441
|
```
|
|
423
442
|
|
|
424
|
-
Subcommand tasks have all the same access to
|
|
443
|
+
Subcommand tasks have all the same access to `sh`, `shebang`, `depends_on`, and the built-in `--debug`/`--verbose` class options as normal tasks. `@@` class variables declared on `Tasks` are also visible in subcommand subclasses.
|
|
425
444
|
|
|
426
445
|
`depends_on` only works within a subcommand group exactly as it does at the top level:
|
|
427
446
|
|
|
428
447
|
```ruby
|
|
429
448
|
class DBCommands < Tasks
|
|
430
|
-
desc "
|
|
449
|
+
desc "Run pending migrations"
|
|
431
450
|
def migrate = sh "rails db:migrate"
|
|
432
451
|
|
|
433
|
-
desc "
|
|
452
|
+
desc "Load seed data"
|
|
434
453
|
def seed = sh "rails db:seed"
|
|
435
454
|
|
|
436
455
|
depends_on :migrate, :seed
|
|
437
|
-
desc "
|
|
456
|
+
desc "Migrate then seed"
|
|
438
457
|
def reset = puts "Done."
|
|
439
458
|
end
|
|
440
459
|
|
|
@@ -470,7 +489,7 @@ Common `method_option` keys: `aliases`, `type`, `default`, `required`, `desc`, `
|
|
|
470
489
|
|
|
471
490
|
## Task files
|
|
472
491
|
|
|
473
|
-
Asgard searches the current directory and its ancestors for a `.loki` file. That file marks the project root. `*.loki` files
|
|
492
|
+
Asgard searches the current directory and its ancestors for a `.loki` file. That file marks the project root. Additional `*.loki` files are loaded only when your `.loki` file explicitly calls `import`.
|
|
474
493
|
|
|
475
494
|
### Single file
|
|
476
495
|
|
|
@@ -485,16 +504,23 @@ Split tasks across files — each reopens `class Tasks`:
|
|
|
485
504
|
|
|
486
505
|
```
|
|
487
506
|
myproject/
|
|
488
|
-
.loki ← entry point
|
|
507
|
+
.loki ← entry point; calls import to load task files
|
|
489
508
|
build.loki
|
|
490
509
|
deploy.loki
|
|
491
510
|
test.loki
|
|
492
511
|
```
|
|
493
512
|
|
|
513
|
+
The `.loki` entry file must call `import` to load the other task files. It also marks the project root for auto-discovery:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
# .loki
|
|
517
|
+
import "*.loki"
|
|
518
|
+
```
|
|
519
|
+
|
|
494
520
|
```ruby
|
|
495
521
|
# build.loki
|
|
496
522
|
class Tasks
|
|
497
|
-
desc "
|
|
523
|
+
desc "Compile the project"
|
|
498
524
|
def build = sh "rake build"
|
|
499
525
|
end
|
|
500
526
|
```
|
|
@@ -503,7 +529,7 @@ end
|
|
|
503
529
|
# test.loki
|
|
504
530
|
class Tasks
|
|
505
531
|
depends_on :build
|
|
506
|
-
desc "
|
|
532
|
+
desc "Run the test suite"
|
|
507
533
|
def test = sh "bundle exec rake test"
|
|
508
534
|
end
|
|
509
535
|
```
|
|
@@ -512,21 +538,21 @@ end
|
|
|
512
538
|
# deploy.loki
|
|
513
539
|
class Tasks
|
|
514
540
|
depends_on :test
|
|
515
|
-
desc "
|
|
541
|
+
desc "Deploy to production"
|
|
516
542
|
def deploy = sh "cap production deploy"
|
|
517
543
|
end
|
|
518
544
|
```
|
|
519
545
|
|
|
520
|
-
|
|
546
|
+
In a single-file project, `.loki` can be completely empty — its presence alone marks the project root. In a multi-file project, add at least an `import` call.
|
|
521
547
|
|
|
522
548
|
### Explicit loading
|
|
523
549
|
|
|
524
|
-
Load any Ruby or `.loki` file manually from `.loki
|
|
550
|
+
Load any Ruby or `.loki` file manually from `.loki`. Use `require_relative` for plain Ruby; use `import` for `.loki` files (it enforces the extension and is idempotent):
|
|
525
551
|
|
|
526
552
|
```ruby
|
|
527
553
|
# .loki
|
|
528
554
|
require_relative "shared/helpers"
|
|
529
|
-
|
|
555
|
+
import "ci.loki"
|
|
530
556
|
|
|
531
557
|
class Tasks
|
|
532
558
|
# additional tasks
|
|
@@ -541,7 +567,6 @@ end
|
|
|
541
567
|
|---|---|
|
|
542
568
|
| `Asgard.run!(argv)` | Entry point — finds `.loki`, loads task files, starts CLI |
|
|
543
569
|
| `Asgard.find_task_file` | Returns path to `.loki` searching from CWD upward, or nil |
|
|
544
|
-
| `Asgard.load_loki(dir)` | Loads all `*.loki` files in dir alphabetically — called by `run!` only when `--auto-load` is passed |
|
|
545
570
|
|
|
546
571
|
`run!` handles its own errors — a missing `.loki`, a circular dependency, or a `depends_on` that names a task that doesn't exist all produce a clean one-line message and exit 1.
|
|
547
572
|
|
data/Rakefile
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "minitest/test_task"
|
|
5
5
|
|
|
6
|
-
SIMPLECOV_PRELUDE = <<~RUBY
|
|
6
|
+
SIMPLECOV_PRELUDE = <<~RUBY
|
|
7
7
|
require "simplecov"
|
|
8
8
|
SimpleCov.start do
|
|
9
9
|
add_filter "/test/"
|
|
@@ -15,8 +15,87 @@ Minitest::TestTask.create do |t|
|
|
|
15
15
|
t.test_prelude = SIMPLECOV_PRELUDE
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
task
|
|
19
|
-
|
|
18
|
+
task default: :test
|
|
19
|
+
|
|
20
|
+
RUBOCOP_ENV = { "RUBOCOP_CACHE_ROOT" => "tmp/rubocop_cache" }.freeze
|
|
21
|
+
|
|
22
|
+
desc "Check code style with RuboCop"
|
|
23
|
+
task :rubocop do
|
|
24
|
+
sh RUBOCOP_ENV, "bundle exec rubocop"
|
|
20
25
|
end
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
desc "Auto-correct RuboCop offenses"
|
|
28
|
+
task :rubocop_fix do
|
|
29
|
+
sh RUBOCOP_ENV, "bundle exec rubocop -a"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "Check code complexity with Flog (warn >=20, fail >=50)"
|
|
33
|
+
task :flog_check do
|
|
34
|
+
require "flog"
|
|
35
|
+
|
|
36
|
+
# Target to work toward; methods above this are warned but don't fail the gate.
|
|
37
|
+
METHOD_WARN = 20.0
|
|
38
|
+
# Current baseline floor — established from first run. Reduce incrementally.
|
|
39
|
+
METHOD_FAIL = 50.0
|
|
40
|
+
|
|
41
|
+
flogger = Flog.new(all: true)
|
|
42
|
+
flogger.flog(*Dir.glob("lib/**/*.rb"))
|
|
43
|
+
|
|
44
|
+
warnings = []
|
|
45
|
+
failures = []
|
|
46
|
+
|
|
47
|
+
flogger.each_by_score do |method, score|
|
|
48
|
+
next if method.end_with?("#none")
|
|
49
|
+
if score > METHOD_FAIL
|
|
50
|
+
failures << "#{"%.1f" % score}: #{method}"
|
|
51
|
+
elsif score > METHOD_WARN
|
|
52
|
+
warnings << "#{"%.1f" % score}: #{method}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
unless warnings.empty?
|
|
57
|
+
puts "\nFlog warnings (#{METHOD_WARN}–#{METHOD_FAIL}) — target for future refactoring:"
|
|
58
|
+
warnings.each { |v| puts " #{v}" }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if failures.empty?
|
|
62
|
+
puts "\nFlog: no methods exceed the failure threshold (>=#{METHOD_FAIL})"
|
|
63
|
+
else
|
|
64
|
+
puts "\nFlog failures (>=#{METHOD_FAIL}) — must be refactored:"
|
|
65
|
+
failures.each { |v| puts " #{v}" }
|
|
66
|
+
$stdout.flush
|
|
67
|
+
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{METHOD_FAIL}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc "Run all quality checks: tests (with coverage), RuboCop, and Flog"
|
|
72
|
+
task :quality do
|
|
73
|
+
results = {}
|
|
74
|
+
|
|
75
|
+
puts "\n#{"=" * 60}"
|
|
76
|
+
puts "Quality Gate: Tests + Coverage"
|
|
77
|
+
puts "=" * 60
|
|
78
|
+
results[:tests] = system("bundle exec rake test") ? :pass : :fail
|
|
79
|
+
|
|
80
|
+
puts "\n#{"=" * 60}"
|
|
81
|
+
puts "Quality Gate: RuboCop"
|
|
82
|
+
puts "=" * 60
|
|
83
|
+
results[:rubocop] = system(RUBOCOP_ENV, "bundle exec rubocop") ? :pass : :fail
|
|
84
|
+
|
|
85
|
+
puts "\n#{"=" * 60}"
|
|
86
|
+
puts "Quality Gate: Flog Complexity"
|
|
87
|
+
puts "=" * 60
|
|
88
|
+
results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
|
|
89
|
+
|
|
90
|
+
puts "\n#{"=" * 60}"
|
|
91
|
+
puts "Quality Summary"
|
|
92
|
+
puts "=" * 60
|
|
93
|
+
results.each do |gate, status|
|
|
94
|
+
icon = status == :pass ? "PASS" : "FAIL"
|
|
95
|
+
puts " [#{icon}] #{gate}"
|
|
96
|
+
end
|
|
97
|
+
puts "=" * 60
|
|
98
|
+
|
|
99
|
+
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
100
|
+
puts "\nAll quality gates passed."
|
|
101
|
+
end
|
data/docs/api.md
CHANGED
|
@@ -12,7 +12,6 @@ These class methods are defined on the `Asgard` module itself.
|
|
|
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
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
|
-
| `load_loki` | `Asgard.load_loki(dir)` | Loads all `*.loki` files in `dir` alphabetically, excluding `.loki` itself. Called by `run!` only when `--auto-load` is present in `argv`. |
|
|
16
15
|
|
|
17
16
|
### `run!` Details
|
|
18
17
|
|
|
@@ -30,6 +29,82 @@ After loading task files, it calls `Tasks.validate_deps!` (circular dependency c
|
|
|
30
29
|
|
|
31
30
|
---
|
|
32
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
|
+
|
|
33
108
|
## `Asgard::Base` DSL Class Methods
|
|
34
109
|
|
|
35
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).
|
|
@@ -37,8 +112,6 @@ After loading task files, it calls `Tasks.validate_deps!` (circular dependency c
|
|
|
37
112
|
| Method | Signature | Description |
|
|
38
113
|
|---|---|---|
|
|
39
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. |
|
|
40
|
-
| `var` | `var(name, value = nil, &block)` | Declare a named variable. If `value` responds to `call` (lambda/proc) or a block is given, the value is computed lazily on first access. Accessible in task bodies as a method. |
|
|
41
|
-
| `import` | `import(mod)` | Include a module into the current class (thin alias for `include`). |
|
|
42
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. |
|
|
43
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. |
|
|
44
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. |
|
|
@@ -65,9 +138,8 @@ depends_on :setup, [:lint, :build], :test # setup, then lint+build concurrently
|
|
|
65
138
|
| `class_option :debug` | class option | `--debug` flag. Sets `$DEBUG = true` before any task runs. Boolean, default `false`. |
|
|
66
139
|
| `class_option :verbose` | class option | `--verbose` flag. Sets `$VERBOSE = true` before any task runs. Boolean, default `false`. |
|
|
67
140
|
| `_version` | private task method | Implements `--version`. Prints `Asgard::VERSION` and exits. Registered via `map "--version" => :_version`. Uses `_` prefix convention. |
|
|
68
|
-
| `debug?` |
|
|
69
|
-
| `verbose?` |
|
|
70
|
-
| `--auto-load` | CLI flag (consumed by `run!`) | Triggers loading of all `*.loki` files before the main `.loki` and the requested task. Consumed by `run!` before Thor dispatch. |
|
|
141
|
+
| `debug?` | Kernel module function | Returns `$DEBUG`. Available everywhere via `Kernel`. |
|
|
142
|
+
| `verbose?` | Kernel module function | Returns `$VERBOSE`. Available everywhere via `Kernel`. |
|
|
71
143
|
|
|
72
144
|
---
|
|
73
145
|
|
|
@@ -78,9 +150,10 @@ These are implementation details exposed for extensibility. Prefer the DSL metho
|
|
|
78
150
|
| Method | Description |
|
|
79
151
|
|---|---|
|
|
80
152
|
| `_deps` | Hash mapping task name symbols to their stage arrays. Set by `depends_on` + `method_added`. |
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
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. |
|
|
84
157
|
| `_build_dep_graph(stages)` | Translates the stage array (from `_deps`) into a Dagwood-compatible hash. |
|
|
85
158
|
|
|
86
159
|
---
|
|
@@ -90,10 +163,10 @@ These are implementation details exposed for extensibility. Prefer the DSL metho
|
|
|
90
163
|
`Asgard::Base` overrides Thor's `invoke_command` to implement dependency resolution and deduplication:
|
|
91
164
|
|
|
92
165
|
1. Sets `$DEBUG` / `$VERBOSE` from `options` if the corresponding flags are present.
|
|
93
|
-
2.
|
|
94
|
-
3.
|
|
95
|
-
4.
|
|
96
|
-
5.
|
|
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.
|
|
97
170
|
|
|
98
171
|
---
|
|
99
172
|
|
data/docs/changelog.md
CHANGED
|
@@ -8,6 +8,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Asg
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
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
|
+
|
|
11
15
|
## [0.2.0] — 2026-05-29
|
|
12
16
|
|
|
13
17
|
### Changed
|