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/README.md
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
# Asgard
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
>
|
|
3
|
+
> [!INFO]
|
|
4
|
+
> See the [CHANGELOG](CHANGELOG.md) for the latest changes. The [examples directory](examples/) contains working `.loki` files demonstrating the full feature set.
|
|
5
|
+
|
|
6
|
+
<br>
|
|
7
|
+
<table>
|
|
8
|
+
<tr>
|
|
9
|
+
<td width="40%" align="center" valign="top">
|
|
10
|
+
<img src="docs/assets/images/asgard.jpg" alt="Asgard"><br>
|
|
11
|
+
<em>"Loki writes the tricks. Asgard runs them."</em>
|
|
12
|
+
</td>
|
|
13
|
+
<td width="60%" valign="top">
|
|
14
|
+
<strong>Key Features</strong><br>
|
|
15
|
+
|
|
16
|
+
- <strong>Thor-Powered CLI</strong> — every Thor DSL feature available inside <code>.loki</code> task files<br>
|
|
17
|
+
- <strong>Task Dependencies</strong> — sequential, parallel, and mixed dependency graphs via <code>depends_on</code><br>
|
|
18
|
+
- <strong>Concurrent Execution</strong> — parallel task groups run in native Ruby threads<br>
|
|
19
|
+
- <strong>Subcommands</strong> — group related tasks under a named namespace<br>
|
|
20
|
+
- <strong>Variables</strong> — shared configuration via Ruby class variables (<code>@@name</code>), visible across all tasks and subcommands<br>
|
|
21
|
+
- <strong>Shell Helpers</strong> — <code>sh</code> for any shell command or heredoc; <code>shebang</code> for polyglot scripts<br>
|
|
22
|
+
- <strong>Dotenv Support</strong> — load <code>.env</code> files into the environment with <code>dotenv</code><br>
|
|
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 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
|
+
</td>
|
|
27
|
+
</tr>
|
|
28
|
+
</table>
|
|
29
|
+
|
|
30
|
+
<p>Asgard is a <a href="https://github.com/rails/thor">Thor</a>-based task runner for Ruby projects. Define tasks in <code>.loki</code> files, declare dependencies between them, and let Asgard handle ordering and concurrent execution. Anything Thor can do — subcommands, typed options, argument validation — is available inside a <code>.loki</code> file.</p>
|
|
8
31
|
|
|
9
32
|
## Installation
|
|
10
33
|
|
|
@@ -28,8 +51,8 @@ Every `.loki` file defines tasks as methods inside `class Tasks`. The `Tasks` cl
|
|
|
28
51
|
|
|
29
52
|
```ruby
|
|
30
53
|
class Tasks
|
|
31
|
-
desc "
|
|
32
|
-
def hello =
|
|
54
|
+
desc "Say hello"
|
|
55
|
+
def hello = puts "Hello, World!"
|
|
33
56
|
end
|
|
34
57
|
```
|
|
35
58
|
|
|
@@ -44,7 +67,7 @@ Declare positional parameters directly in the method signature. Document them in
|
|
|
44
67
|
```ruby
|
|
45
68
|
class Tasks
|
|
46
69
|
desc "hello NAME", "Say hello to NAME"
|
|
47
|
-
def hello(name = "World") =
|
|
70
|
+
def hello(name = "World") = puts "Hello, #{name}!"
|
|
48
71
|
end
|
|
49
72
|
```
|
|
50
73
|
|
|
@@ -55,7 +78,7 @@ asgard hello Alice
|
|
|
55
78
|
|
|
56
79
|
### A task with a formal argument declaration
|
|
57
80
|
|
|
58
|
-
Use `argument` for richer metadata — type checking, enums, and help text:
|
|
81
|
+
Use `argument` for richer metadata — type checking, enums, and help text. **Warning: `argument` is a class-level declaration that applies to every task in the class**, not just the one below it. It is best suited for single-command CLIs or when every task genuinely shares the same positional input. In multi-task files, prefer method signature parameters instead.
|
|
59
82
|
|
|
60
83
|
```ruby
|
|
61
84
|
class Tasks
|
|
@@ -65,7 +88,7 @@ class Tasks
|
|
|
65
88
|
desc: "Name to greet"
|
|
66
89
|
|
|
67
90
|
desc "hello NAME", "Say hello to NAME"
|
|
68
|
-
def hello =
|
|
91
|
+
def hello = puts "Hello, #{name}!"
|
|
69
92
|
end
|
|
70
93
|
```
|
|
71
94
|
|
|
@@ -80,7 +103,7 @@ class Tasks
|
|
|
80
103
|
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
|
|
81
104
|
def hello(name = "World")
|
|
82
105
|
message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
|
|
83
|
-
options[:count].times {
|
|
106
|
+
options[:count].times { puts message }
|
|
84
107
|
end
|
|
85
108
|
end
|
|
86
109
|
```
|
|
@@ -105,7 +128,7 @@ class Tasks
|
|
|
105
128
|
method_option :count, aliases: "-n", type: :numeric, default: 1, desc: "Repeat N times"
|
|
106
129
|
def hello(name = "World")
|
|
107
130
|
message = options[:shout] ? "HELLO, #{name.upcase}!" : "Hello, #{name}!"
|
|
108
|
-
options[:count].times {
|
|
131
|
+
options[:count].times { puts message }
|
|
109
132
|
end
|
|
110
133
|
end
|
|
111
134
|
```
|
|
@@ -124,15 +147,15 @@ Bare symbols run one after another in the order declared:
|
|
|
124
147
|
|
|
125
148
|
```ruby
|
|
126
149
|
class Tasks
|
|
127
|
-
desc "
|
|
150
|
+
desc "Compile the project"
|
|
128
151
|
def build = sh "rake build"
|
|
129
152
|
|
|
130
153
|
depends_on :build
|
|
131
|
-
desc "
|
|
154
|
+
desc "Run the test suite"
|
|
132
155
|
def test = sh "rake test"
|
|
133
156
|
|
|
134
157
|
depends_on :test
|
|
135
|
-
desc "
|
|
158
|
+
desc "Publish the gem"
|
|
136
159
|
def release = sh "bundle exec rake release"
|
|
137
160
|
end
|
|
138
161
|
```
|
|
@@ -147,15 +170,15 @@ Wrap symbols in an array to declare they can run concurrently. Asgard waits for
|
|
|
147
170
|
|
|
148
171
|
```ruby
|
|
149
172
|
class Tasks
|
|
150
|
-
desc "
|
|
173
|
+
desc "Check code style"
|
|
151
174
|
def lint = sh "bundle exec rubocop"
|
|
152
175
|
|
|
153
|
-
desc "
|
|
176
|
+
desc "Run type checks"
|
|
154
177
|
def typecheck = sh "bundle exec srb tc"
|
|
155
178
|
|
|
156
179
|
# lint and typecheck run in parallel, test waits for both
|
|
157
180
|
depends_on [:lint, :typecheck]
|
|
158
|
-
desc "
|
|
181
|
+
desc "Run the test suite"
|
|
159
182
|
def test = sh "bundle exec rake test"
|
|
160
183
|
end
|
|
161
184
|
```
|
|
@@ -170,16 +193,16 @@ Mix bare symbols (sequential) and arrays (parallel) in a single `depends_on` cal
|
|
|
170
193
|
|
|
171
194
|
```ruby
|
|
172
195
|
class Tasks
|
|
173
|
-
desc "
|
|
174
|
-
desc "
|
|
175
|
-
desc "
|
|
176
|
-
desc "
|
|
177
|
-
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\"}'"
|
|
178
201
|
|
|
179
202
|
# setup first, then lint+build in parallel, then test, then notify
|
|
180
203
|
depends_on :setup, [:lint, :build], :test, :notify
|
|
181
|
-
desc "
|
|
182
|
-
def ci =
|
|
204
|
+
desc "Full CI pipeline"
|
|
205
|
+
def ci = puts "CI complete"
|
|
183
206
|
end
|
|
184
207
|
```
|
|
185
208
|
|
|
@@ -201,18 +224,24 @@ asgard ci executes:
|
|
|
201
224
|
|
|
202
225
|
## Variables
|
|
203
226
|
|
|
204
|
-
|
|
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:
|
|
205
228
|
|
|
206
229
|
```ruby
|
|
207
230
|
class Tasks
|
|
208
|
-
|
|
209
|
-
|
|
231
|
+
@@app ||= "myapp".freeze
|
|
232
|
+
@@max_jobs ||= 4
|
|
233
|
+
|
|
234
|
+
desc "Create a release tag"
|
|
235
|
+
def tag = sh "git tag #{@@app}-#{version}"
|
|
236
|
+
|
|
237
|
+
private
|
|
210
238
|
|
|
211
|
-
|
|
212
|
-
def tag = sh "git tag #{app}-#{version}"
|
|
239
|
+
def version = `git describe --tags`.strip
|
|
213
240
|
end
|
|
214
241
|
```
|
|
215
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
|
+
|
|
216
245
|
---
|
|
217
246
|
|
|
218
247
|
## Helper methods
|
|
@@ -221,13 +250,13 @@ Private methods are callable from any task in the same class but are never regis
|
|
|
221
250
|
|
|
222
251
|
```ruby
|
|
223
252
|
class Tasks
|
|
224
|
-
desc "
|
|
253
|
+
desc "Compile and package"
|
|
225
254
|
def build
|
|
226
255
|
compile("src")
|
|
227
256
|
package(version)
|
|
228
257
|
end
|
|
229
258
|
|
|
230
|
-
desc "
|
|
259
|
+
desc "Build and publish"
|
|
231
260
|
def release
|
|
232
261
|
build
|
|
233
262
|
sh "gem push pkg/myapp-#{version}.gem"
|
|
@@ -263,7 +292,7 @@ require_relative "shared/helpers"
|
|
|
263
292
|
class Tasks
|
|
264
293
|
include BuildHelpers
|
|
265
294
|
|
|
266
|
-
desc "
|
|
295
|
+
desc "Compile the project"
|
|
267
296
|
def build = compile("src")
|
|
268
297
|
end
|
|
269
298
|
```
|
|
@@ -297,7 +326,7 @@ end
|
|
|
297
326
|
|
|
298
327
|
```ruby
|
|
299
328
|
class Tasks
|
|
300
|
-
desc "
|
|
329
|
+
desc "Bootstrap the development environment"
|
|
301
330
|
def setup
|
|
302
331
|
sh <<~SHELL
|
|
303
332
|
brew install redis postgresql
|
|
@@ -307,7 +336,7 @@ class Tasks
|
|
|
307
336
|
SHELL
|
|
308
337
|
end
|
|
309
338
|
|
|
310
|
-
desc "
|
|
339
|
+
desc "Run Python data analysis"
|
|
311
340
|
def analyze
|
|
312
341
|
shebang :python3, <<~PYTHON
|
|
313
342
|
import json
|
|
@@ -316,7 +345,7 @@ class Tasks
|
|
|
316
345
|
PYTHON
|
|
317
346
|
end
|
|
318
347
|
|
|
319
|
-
desc "
|
|
348
|
+
desc "Build frontend assets with esbuild"
|
|
320
349
|
def bundle_assets
|
|
321
350
|
shebang :node, <<~JS
|
|
322
351
|
const esbuild = require("esbuild")
|
|
@@ -328,28 +357,45 @@ end
|
|
|
328
357
|
|
|
329
358
|
Supported interpreters: `:python3`, `:python`, `:node`, `:ruby`, `:perl`, `:bash`, `:sh`. Any other symbol is passed directly to `system` with a `.tmp` extension.
|
|
330
359
|
|
|
331
|
-
Pass `silent: true` to suppress the
|
|
360
|
+
Pass `silent: true` to both `sh` and `shebang` to suppress the script echo:
|
|
332
361
|
|
|
333
362
|
```ruby
|
|
334
|
-
def build
|
|
363
|
+
def build = sh "rake build", silent: true
|
|
364
|
+
def analyze = shebang :python3, <<~PY, silent: true
|
|
365
|
+
import json
|
|
366
|
+
print(json.load(open("data.json")))
|
|
367
|
+
PY
|
|
335
368
|
```
|
|
336
369
|
|
|
337
370
|
---
|
|
338
371
|
|
|
339
372
|
## Environment variables
|
|
340
373
|
|
|
341
|
-
`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:
|
|
342
375
|
|
|
343
376
|
```ruby
|
|
344
377
|
class Tasks
|
|
345
378
|
dotenv # loads .env
|
|
346
379
|
dotenv ".env.local" # or a specific file
|
|
347
380
|
|
|
348
|
-
desc "
|
|
349
|
-
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
|
|
350
390
|
end
|
|
351
391
|
```
|
|
352
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
|
+
|
|
353
399
|
---
|
|
354
400
|
|
|
355
401
|
## Command aliases
|
|
@@ -362,7 +408,7 @@ class Tasks
|
|
|
362
408
|
map "--v" => "version"
|
|
363
409
|
map "t" => "test"
|
|
364
410
|
|
|
365
|
-
desc "
|
|
411
|
+
desc "Print the version"
|
|
366
412
|
def version = puts Asgard::VERSION
|
|
367
413
|
end
|
|
368
414
|
```
|
|
@@ -375,10 +421,10 @@ Group related tasks under a common name using Thor's `subcommand` method. Define
|
|
|
375
421
|
|
|
376
422
|
```ruby
|
|
377
423
|
class DeployCommands < Tasks
|
|
378
|
-
desc "
|
|
424
|
+
desc "Deploy to staging"
|
|
379
425
|
def staging = sh "cap staging deploy"
|
|
380
426
|
|
|
381
|
-
desc "
|
|
427
|
+
desc "Deploy to production"
|
|
382
428
|
def production = sh "cap production deploy"
|
|
383
429
|
end
|
|
384
430
|
|
|
@@ -394,20 +440,20 @@ asgard deploy staging
|
|
|
394
440
|
asgard deploy production
|
|
395
441
|
```
|
|
396
442
|
|
|
397
|
-
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.
|
|
398
444
|
|
|
399
445
|
`depends_on` only works within a subcommand group exactly as it does at the top level:
|
|
400
446
|
|
|
401
447
|
```ruby
|
|
402
448
|
class DBCommands < Tasks
|
|
403
|
-
desc "
|
|
449
|
+
desc "Run pending migrations"
|
|
404
450
|
def migrate = sh "rails db:migrate"
|
|
405
451
|
|
|
406
|
-
desc "
|
|
452
|
+
desc "Load seed data"
|
|
407
453
|
def seed = sh "rails db:seed"
|
|
408
454
|
|
|
409
455
|
depends_on :migrate, :seed
|
|
410
|
-
desc "
|
|
456
|
+
desc "Migrate then seed"
|
|
411
457
|
def reset = puts "Done."
|
|
412
458
|
end
|
|
413
459
|
|
|
@@ -443,7 +489,7 @@ Common `method_option` keys: `aliases`, `type`, `default`, `required`, `desc`, `
|
|
|
443
489
|
|
|
444
490
|
## Task files
|
|
445
491
|
|
|
446
|
-
Asgard searches the current directory and its ancestors for a `.loki` file. That file marks the project root.
|
|
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`.
|
|
447
493
|
|
|
448
494
|
### Single file
|
|
449
495
|
|
|
@@ -458,16 +504,23 @@ Split tasks across files — each reopens `class Tasks`:
|
|
|
458
504
|
|
|
459
505
|
```
|
|
460
506
|
myproject/
|
|
461
|
-
.loki ← entry point
|
|
507
|
+
.loki ← entry point; calls import to load task files
|
|
462
508
|
build.loki
|
|
463
509
|
deploy.loki
|
|
464
510
|
test.loki
|
|
465
511
|
```
|
|
466
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
|
+
|
|
467
520
|
```ruby
|
|
468
521
|
# build.loki
|
|
469
522
|
class Tasks
|
|
470
|
-
desc "
|
|
523
|
+
desc "Compile the project"
|
|
471
524
|
def build = sh "rake build"
|
|
472
525
|
end
|
|
473
526
|
```
|
|
@@ -476,7 +529,7 @@ end
|
|
|
476
529
|
# test.loki
|
|
477
530
|
class Tasks
|
|
478
531
|
depends_on :build
|
|
479
|
-
desc "
|
|
532
|
+
desc "Run the test suite"
|
|
480
533
|
def test = sh "bundle exec rake test"
|
|
481
534
|
end
|
|
482
535
|
```
|
|
@@ -485,21 +538,21 @@ end
|
|
|
485
538
|
# deploy.loki
|
|
486
539
|
class Tasks
|
|
487
540
|
depends_on :test
|
|
488
|
-
desc "
|
|
541
|
+
desc "Deploy to production"
|
|
489
542
|
def deploy = sh "cap production deploy"
|
|
490
543
|
end
|
|
491
544
|
```
|
|
492
545
|
|
|
493
|
-
|
|
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.
|
|
494
547
|
|
|
495
548
|
### Explicit loading
|
|
496
549
|
|
|
497
|
-
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):
|
|
498
551
|
|
|
499
552
|
```ruby
|
|
500
553
|
# .loki
|
|
501
554
|
require_relative "shared/helpers"
|
|
502
|
-
|
|
555
|
+
import "ci.loki"
|
|
503
556
|
|
|
504
557
|
class Tasks
|
|
505
558
|
# additional tasks
|
|
@@ -514,9 +567,8 @@ end
|
|
|
514
567
|
|---|---|
|
|
515
568
|
| `Asgard.run!(argv)` | Entry point — finds `.loki`, loads task files, starts CLI |
|
|
516
569
|
| `Asgard.find_task_file` | Returns path to `.loki` searching from CWD upward, or nil |
|
|
517
|
-
| `Asgard.load_loki(dir)` | Loads all `*.loki` files in dir alphabetically |
|
|
518
570
|
|
|
519
|
-
`run!` handles its own errors — a missing `.loki
|
|
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.
|
|
520
572
|
|
|
521
573
|
---
|
|
522
574
|
|
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
|