asgard 0.3.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 -24
- data/CHANGELOG.md +42 -2
- data/README.md +84 -2
- data/docs/api.md +8 -5
- data/docs/changelog.md +8 -0
- data/docs/helpers.md +84 -0
- data/docs/options.md +40 -6
- data/docs/task-files.md +2 -2
- data/examples/server_subcommands.loki +31 -22
- data/gem_tasks.loki +35 -0
- data/lib/asgard/base.rb +39 -3
- data/lib/asgard/kernel_methods.rb +6 -4
- data/lib/asgard/tasks.rb +12 -6
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +4 -0
- data/quality.loki +75 -0
- metadata +3 -2
- data/Rakefile +0 -101
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d5752f0e0d294dfb08b809b62b0fbcb4d26f281c6c6a9ddc4ca2b9ae9c310cfc
|
|
4
|
+
data.tar.gz: 918cbb04f6b285b0d96f674b963cd98c899f151159a73e8ccba72824c8c485f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4a32a84315d0367316ea813f5106d55e54c465f8d1ff1ca6958955fd8c23153483a0002a5396af63ad980ec37dacba6c2b3b311516901ff6bd0dc8ecd4dea37
|
|
7
|
+
data.tar.gz: 87b809a6e2900d22bec4cff5d9b133a4620bf6c2972312c02243bea916aa6cc01dcf668c3ea80786d710ddf39e7e5c3b773d35ec27577253b131a2e0463e1b39
|
data/.loki
CHANGED
|
@@ -2,33 +2,22 @@
|
|
|
2
2
|
# Asgard gem's own task file.
|
|
3
3
|
# Task is pre-defined by the gem — just reopen it to add tasks.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
desc "Run the test suite"
|
|
9
|
-
def test
|
|
10
|
-
sh "bundle exec rake test"
|
|
11
|
-
end
|
|
5
|
+
import "quality.loki"
|
|
6
|
+
import "gem_tasks.loki"
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
end
|
|
8
|
+
class Tasks
|
|
9
|
+
@@project ||= "asgard".freeze
|
|
10
|
+
@@project_desc ||= "CLI-task runner"
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
sh "bundle exec rake build"
|
|
12
|
+
helper(:project_version) do
|
|
13
|
+
@@project_version ||= File.read("lib/#{@@project}/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
21
14
|
end
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
desc "Build and install gem locally"
|
|
25
|
-
def install
|
|
26
|
-
sh "bundle exec rake install"
|
|
27
|
-
end
|
|
16
|
+
default_task :quality
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
header <<~HEAD
|
|
19
|
+
Porject: #{@@project} (v#{project_version}) - #{@@project_desc}
|
|
20
|
+
Root Dir: #{loki_up.parent}
|
|
21
|
+
Default task: #{default_task}
|
|
22
|
+
HEAD
|
|
34
23
|
end
|
data/CHANGELOG.md
CHANGED
|
@@ -5,10 +5,50 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.3.
|
|
8
|
+
## [0.3.1] - Unreleased
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **`helper(name, &block)` DSL method on `Asgard::Base`** — defines a method available in both class context (e.g. inside `header` or `footer`) and as a private instance method inside task bodies, with a single declaration. Eliminates the manual `def self.name` + `no_commands { private def name = self.class.name }` boilerplate. Supports positional arguments, keyword arguments, default values, and block arguments.
|
|
13
|
+
```ruby
|
|
14
|
+
class Tasks
|
|
15
|
+
@@project ||= "myapp".freeze
|
|
16
|
+
|
|
17
|
+
helper(:version) {
|
|
18
|
+
File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
header "#{@@project} v#{version}" # class context
|
|
22
|
+
|
|
23
|
+
desc "Show the current version"
|
|
24
|
+
def show_version = puts version # instance context
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **`header(text)` and `footer(text)` DSL methods on `Asgard::Base`** — attach static text to the general help output. `header` lines are printed above the commands list; `footer` lines are printed below the options block. Multiple calls accumulate: each `header` call appends a line, each `footer` call prepends a line, so content from later-loaded files naturally wraps around content from earlier files. Neither appears when `asgard help <command>` is called for per-command detail.
|
|
29
|
+
```ruby
|
|
30
|
+
class Tasks
|
|
31
|
+
header "my-project — build & release tasks"
|
|
32
|
+
footer "See https://example.com/docs for details"
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
- **`no_negate(*names)` DSL method on `Asgard::Base`** — suppresses the `[--no-name]` and `[--skip-name]` negation variants from help output for boolean class options where negation is meaningless. Call it after the `class_option` declaration:
|
|
36
|
+
```ruby
|
|
37
|
+
class_option :version, type: :boolean, default: false, desc: "Show version and exit"
|
|
38
|
+
no_negate :version
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **`--version` reimplemented as a `class_option`** — the flag now appears in the "Options" section of `asgard help` alongside `--debug` and `--verbose`, rather than as a listed command. The `_version` method and its `map "--version" => :_version` registration have been removed. The actual early-exit behaviour still lives in `Asgard.run!` (before the `.loki` file is required), so `--version` works even when no `.loki` file exists. `no_negate :version` suppresses the spurious `[--no-version]` / `[--skip-version]` variants.
|
|
44
|
+
- **`loki_up` returns `Pathname` instead of `String`** — the return value is now a `Pathname` instance (or `nil` when not found). `Pathname` is accepted everywhere `loki_up`'s result is used: `import`, `dotenv`, `load`, and standard Ruby file methods all accept `Pathname` via `to_path` / `to_s`. Code that passes the result directly to those methods is unaffected; code that performs string operations on the path should call `.to_s` first.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **Help output showed `tasks` prefix before every command** — `asgard help` displayed `asgard tasks build` instead of `asgard build`. The cause was a keyword-vs-positional mismatch in the `Asgard::Base#help` override: declaring `subcommand: false` as a keyword argument caused Ruby's `super` to forward it as the hash `{subcommand: false}`. Thor's `banner` method received this hash as the positional `subcommand` argument, treated it as truthy, and prepended the class namespace (`tasks`) to every command name. Fixed by changing the override signature to match Thor's positional signature: `def help(command = nil, subcommand = false)`.
|
|
49
|
+
|
|
50
|
+
### Added (continued)
|
|
51
|
+
|
|
12
52
|
- **`loki_up(name = ".loki")` Kernel method** — searches `Dir.pwd` and each ancestor directory for a file with the given name; returns the absolute path of the first match or `nil`. Available everywhere in Ruby (task bodies, `.loki` files, top-level code) as a `module_function` on `Kernel`.
|
|
13
53
|
- **`import(path)` Kernel method** — loads a `.loki` file (or a glob of `.loki` files) with `require`-like idempotency via `$LOADED_FEATURES`. Accepts a `String` or `Pathname`. Relative paths are resolved relative to the caller's file (like `require_relative`). Glob patterns (`*.loki`, `**/*.loki`) expand via `Dir.glob` and load all matches. Returns `true` if any file was newly loaded, `false` if all were already loaded or no glob matches were found. Raises `ArgumentError` if the path does not end with `.loki`.
|
|
14
54
|
- **`import_up(name = ".loki")` Kernel method** — combines `loki_up` and `import`. For exact names, finds the first ancestor directory containing that file and loads it. For glob names, finds the first ancestor directory containing any matching files and loads them all — stopping at that level rather than aggregating across multiple ancestors. Returns `false` if nothing is found.
|
|
@@ -16,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
56
|
- **`env(name, default = nil)` Kernel method** — fetches a system environment variable by symbol or string name, upcasing the key automatically. `env(:port, "3000")` returns `"3000"` when `PORT` is unset; `env(:api_key)` raises `KeyError` when `API_KEY` is missing and no default is provided. Accepts both `env(:port)` and `env("PORT")` forms. Cleaner than `ENV['PORT']` in task bodies.
|
|
17
57
|
- **Verbose/debug feedback for `import` and `import_up`** — when `verbose?` is true, each file loaded is printed to stderr. When `debug?` is true, already-loaded files are also reported (with an "already loaded" suffix), and `import_up` reports when a file is not found.
|
|
18
58
|
- **RuboCop lint gate** — RuboCop is now a first-class quality gate alongside tests and Flog. Added `rubocop` to the Gemfile, a `.rubocop.yml` tuned for this codebase (Ruby 3.2 target, relaxed `Metrics` thresholds consistent with Flog as the primary complexity gate, `examples/` excluded), and `rake rubocop` / `rake rubocop_fix` tasks backed by a `tmp/rubocop_cache` directory for fast re-runs.
|
|
19
|
-
- **Expanded `rake quality` task** — `quality` now runs three independent gates (tests + coverage, RuboCop, Flog) and
|
|
59
|
+
- **Expanded `rake quality` task** — `quality` now runs three independent gates (tests + coverage, RuboCop, Flog) in parallel using `depends_on [:test, :rubocop, :flog_check]`. Each gate captures its pass/fail result in an instance variable; output is suppressed on pass and filtered to failures only on fail, preventing interleaved output from concurrent subprocesses. A formatted pass/fail summary table is printed after all gates complete, so every failure is visible in a single run.
|
|
20
60
|
- **`rake flog_check` task** — replaces the bare `flog lib/` call with a structured task that enforces per-method thresholds (warn ≥20, fail ≥50), lists warnings and failures in separate sections, and exits non-zero only when the failure threshold is breached.
|
|
21
61
|
- **Single-argument `desc` shorthand** — `desc` now accepts one string (the description) with the usage string omitted. The usage defaults to the method name, eliminating the redundant first argument for the common case:
|
|
22
62
|
```ruby
|
data/README.md
CHANGED
|
@@ -18,11 +18,12 @@
|
|
|
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
20
|
- <strong>Variables</strong> — shared configuration via Ruby class variables (<code>@@name</code>), visible across all tasks and subcommands<br>
|
|
21
|
+
- <strong>`helper` DSL</strong> — define a method once, available in both class-level DSL calls (<code>header</code>) and inside task instance methods<br>
|
|
21
22
|
- <strong>Shell Helpers</strong> — <code>sh</code> for any shell command or heredoc; <code>shebang</code> for polyglot scripts<br>
|
|
22
23
|
- <strong>Dotenv Support</strong> — load <code>.env</code> files into the environment with <code>dotenv</code><br>
|
|
23
24
|
- <strong>Auto-Discovery</strong> — <code>.loki</code> root marker searched from CWD upward through parent directories<br>
|
|
24
25
|
- <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
|
|
26
|
+
- <strong>Built-in Flags</strong> — <code>--debug</code>, <code>--verbose</code>, and <code>--version</code> built-in class options; header/footer DSL for static help text<br>
|
|
26
27
|
</td>
|
|
27
28
|
</tr>
|
|
28
29
|
</table>
|
|
@@ -274,6 +275,40 @@ class Tasks
|
|
|
274
275
|
end
|
|
275
276
|
```
|
|
276
277
|
|
|
278
|
+
### The `helper` DSL method
|
|
279
|
+
|
|
280
|
+
Some values need to be available in both class context (e.g. inside `header`) and inside task instance methods. `helper` defines the method once in both contexts:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
class Tasks
|
|
284
|
+
@@project ||= "myapp".freeze
|
|
285
|
+
|
|
286
|
+
helper(:version) {
|
|
287
|
+
File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
header "#{@@project} v#{version}" # class context
|
|
291
|
+
|
|
292
|
+
desc "Show the current version"
|
|
293
|
+
def show_version
|
|
294
|
+
puts version # instance context
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Without `helper`, achieving this requires two separate definitions:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
def self.version = File.read(...).match(...)[1].freeze
|
|
303
|
+
no_commands { private def version = self.class.version }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`helper` accepts positional arguments, keyword arguments, and blocks — any signature valid in a Ruby method definition:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
helper(:tag) { |name, ver, prefix: "v"| "#{prefix}#{name}-#{ver}" }
|
|
310
|
+
```
|
|
311
|
+
|
|
277
312
|
Helpers can also be shared across multiple `.loki` files by extracting them into a plain Ruby file and loading it explicitly:
|
|
278
313
|
|
|
279
314
|
```ruby
|
|
@@ -299,6 +334,53 @@ end
|
|
|
299
334
|
|
|
300
335
|
---
|
|
301
336
|
|
|
337
|
+
## Help header and footer
|
|
338
|
+
|
|
339
|
+
Add static text above and below the command list in `asgard help` output:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
class Tasks
|
|
343
|
+
header "my-project — build & release tasks"
|
|
344
|
+
footer "See https://example.com/docs for details"
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
Multiple calls accumulate. `header` appends each line (top to bottom); `footer` prepends each line (bottom to top), so content from a later-loaded `.loki` file sits closer to the commands:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# .loki
|
|
352
|
+
class Tasks
|
|
353
|
+
header "my-project"
|
|
354
|
+
footer "Maintainer: you@example.com"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
import "*.loki"
|
|
358
|
+
|
|
359
|
+
# deploy.loki
|
|
360
|
+
class Tasks
|
|
361
|
+
header " deploy targets: staging, production"
|
|
362
|
+
footer "See runbook at wiki/deploy"
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
my-project
|
|
368
|
+
deploy targets: staging, production
|
|
369
|
+
|
|
370
|
+
Commands:
|
|
371
|
+
...
|
|
372
|
+
|
|
373
|
+
Options:
|
|
374
|
+
...
|
|
375
|
+
|
|
376
|
+
See runbook at wiki/deploy
|
|
377
|
+
Maintainer: you@example.com
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Header and footer text is only shown for the general `asgard help` page, not for `asgard help <command>`.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
302
384
|
## Options shared across all tasks
|
|
303
385
|
|
|
304
386
|
`class_option` defines an option available to every task in the class:
|
|
@@ -566,7 +648,7 @@ end
|
|
|
566
648
|
| Method | Description |
|
|
567
649
|
|---|---|
|
|
568
650
|
| `Asgard.run!(argv)` | Entry point — finds `.loki`, loads task files, starts CLI |
|
|
569
|
-
| `Asgard.find_task_file` | Returns
|
|
651
|
+
| `Asgard.find_task_file` | Returns a `Pathname` to `.loki` searching from CWD upward, or `nil` |
|
|
570
652
|
|
|
571
653
|
`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.
|
|
572
654
|
|
data/docs/api.md
CHANGED
|
@@ -11,7 +11,7 @@ These class methods are defined on the `Asgard` module itself.
|
|
|
11
11
|
| Method | Signature | Description |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| `run!` | `Asgard.run!(argv)` | Main entry point. Finds `.loki`, loads all task files, validates the dependency graph, and dispatches via Thor. Handles its own errors: missing `.loki` and circular dependencies both produce a clean one-line message and `exit 1`. |
|
|
14
|
-
| `find_task_file` | `Asgard.find_task_file →
|
|
14
|
+
| `find_task_file` | `Asgard.find_task_file → Pathname, nil` | Searches `Dir.pwd` and each ancestor directory for a `.loki` file. Returns a `Pathname` of the first match, or `nil` if none is found. |
|
|
15
15
|
|
|
16
16
|
### `run!` Details
|
|
17
17
|
|
|
@@ -35,7 +35,7 @@ These methods are defined as `module_function` on `Kernel` and are therefore ava
|
|
|
35
35
|
|
|
36
36
|
| Method | Signature | Returns | Description |
|
|
37
37
|
|---|---|---|---|
|
|
38
|
-
| `loki_up` | `loki_up(name = ".loki") →
|
|
38
|
+
| `loki_up` | `loki_up(name = ".loki") → Pathname, nil` | `Pathname` or `nil` | Searches `Dir.pwd` and each ancestor directory for a file named `name`. Returns a `Pathname` for the first match, or `nil` if not found. Exact filenames only — does not expand globs. |
|
|
39
39
|
| `import` | `import(path) → true, false` | `true` if any file newly loaded | Loads one `.loki` file or a glob of `.loki` files. Relative paths resolve relative to the caller's file (like `require_relative`). Idempotent via `$LOADED_FEATURES`. Raises `ArgumentError` if `path` does not end with `.loki`. Raises `LoadError` if a non-glob path does not exist. |
|
|
40
40
|
| `import_up` | `import_up(name = ".loki") → true, false` | `true` if any file newly loaded | Combines `loki_up` and `import`. Walks ancestors to find the file or glob match, then loads it. Returns `false` if nothing is found. |
|
|
41
41
|
| `debug?` | `debug? → true, false` | `$DEBUG` | Returns the current value of `$DEBUG`. Set to `true` by `--debug` on the CLI or directly via `$DEBUG = true`. |
|
|
@@ -53,11 +53,11 @@ loki_up(".env") # find the nearest .env file up the tree
|
|
|
53
53
|
loki_up("VERSION") # find a VERSION file in CWD or any ancestor
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Returns
|
|
56
|
+
Returns a `Pathname` or `nil`. Does not load the file. `Pathname` is accepted by `import`, `dotenv`, `load`, and standard Ruby file methods — no `.to_s` conversion needed in common usage.
|
|
57
57
|
|
|
58
58
|
```ruby
|
|
59
59
|
if (path = loki_up("gem_tasks.loki"))
|
|
60
|
-
import path
|
|
60
|
+
import path # Pathname accepted directly
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Pass the located .env to dotenv — works from any subdirectory
|
|
@@ -113,6 +113,9 @@ import_up "*.loki" # find the nearest ancestor with *.loki files
|
|
|
113
113
|
|---|---|---|
|
|
114
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
115
|
| `dotenv` | `dotenv(path = ".env")` | Load the specified `.env` file into `ENV` using the dotenv gem. Silently skipped if the file does not exist. Called at class-load time. |
|
|
116
|
+
| `header` | `header(text)` | Append a line of text shown above the commands list in `asgard help`. Each call adds another line. No-op for per-command help. |
|
|
117
|
+
| `footer` | `footer(text)` | Prepend a line of text shown below the options block in `asgard help`. Each call inserts above the previous lines. No-op for per-command help. |
|
|
118
|
+
| `no_negate` | `no_negate(*names)` | Suppress `[--no-name]` / `[--skip-name]` help entries for one or more boolean class options. Call after the `class_option` declaration. |
|
|
116
119
|
| `sh` | `sh(script, silent: false)` | Instance method. Run a shell command or multiline heredoc. Single-line → `system(script)`; multiline → `system("bash", "-c", script)`. Exits with the command's status on failure. |
|
|
117
120
|
| `shebang` | `shebang(interpreter, script, silent: false)` | Instance method. Write `script` to a tempfile and execute it with `interpreter`. See the [Shell Helpers](shell.md) page for the full interpreter table. |
|
|
118
121
|
| `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. |
|
|
@@ -137,7 +140,7 @@ depends_on :setup, [:lint, :build], :test # setup, then lint+build concurrently
|
|
|
137
140
|
|---|---|---|
|
|
138
141
|
| `class_option :debug` | class option | `--debug` flag. Sets `$DEBUG = true` before any task runs. Boolean, default `false`. |
|
|
139
142
|
| `class_option :verbose` | class option | `--verbose` flag. Sets `$VERBOSE = true` before any task runs. Boolean, default `false`. |
|
|
140
|
-
| `
|
|
143
|
+
| `class_option :version` | class option | `--version` flag. Handled by `Asgard.run!` before the `.loki` file is loaded — prints `Asgard::VERSION` and exits. `no_negate :version` suppresses the `[--no-version]` / `[--skip-version]` help entries. |
|
|
141
144
|
| `debug?` | Kernel module function | Returns `$DEBUG`. Available everywhere via `Kernel`. |
|
|
142
145
|
| `verbose?` | Kernel module function | Returns `$VERBOSE`. Available everywhere via `Kernel`. |
|
|
143
146
|
|
data/docs/changelog.md
CHANGED
|
@@ -8,6 +8,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Asg
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`helper` DSL method** — defines a method available in both class context (e.g. inside `header`) and instance context (inside task methods) with a single declaration. Eliminates the manual `def self.name` + `no_commands { private def name = self.class.name }` boilerplate. Supports positional arguments, keyword arguments, and block arguments. See [Helper Methods](helpers.md).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **`quality` task** — all three gates (`test`, `rubocop`, `flog_check`) now run in parallel. Each gate captures its own pass/fail result; output is suppressed on pass and filtered to failures only on fail. A summary table is printed after all gates complete.
|
|
18
|
+
|
|
11
19
|
### Removed
|
|
12
20
|
|
|
13
21
|
- **`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).
|
data/docs/helpers.md
CHANGED
|
@@ -75,6 +75,90 @@ end
|
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
+
## The `helper` DSL Method
|
|
79
|
+
|
|
80
|
+
`helper` is an Asgard DSL method that defines a helper available in **both class context and instance context** with a single declaration. It is the right tool when a value needs to be used inside a `header` or `footer` call (which execute at class load time) and also inside task instance methods.
|
|
81
|
+
|
|
82
|
+
### The problem it solves
|
|
83
|
+
|
|
84
|
+
Thor task methods run as instance methods. Class-level DSL calls like `header` and `footer` run as class methods. A plain `def` only creates an instance method, so it cannot be called inside `header`. Conversely, `def self.name` only creates a class method, so it cannot be called inside a task body without `self.class.name`.
|
|
85
|
+
|
|
86
|
+
The manual workaround is verbose:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class Tasks
|
|
90
|
+
@@project ||= "myapp".freeze
|
|
91
|
+
|
|
92
|
+
# class method for header/footer
|
|
93
|
+
def self.version
|
|
94
|
+
@@version ||= File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# private instance method delegating to the class method
|
|
98
|
+
no_commands do
|
|
99
|
+
private def version = self.class.version
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
header "#{@@project} v#{version}"
|
|
103
|
+
|
|
104
|
+
desc "Show version"
|
|
105
|
+
def show_version = puts version
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`helper` replaces those six lines with one:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
class Tasks
|
|
113
|
+
@@project ||= "myapp".freeze
|
|
114
|
+
|
|
115
|
+
helper(:version) {
|
|
116
|
+
@@version ||= File.read("lib/myapp/version.rb").match(/VERSION\s*=\s*"([^"]+)"/)[1].freeze
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
header "#{@@project} v#{version}"
|
|
120
|
+
|
|
121
|
+
desc "Show version"
|
|
122
|
+
def show_version = puts version
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Arguments
|
|
127
|
+
|
|
128
|
+
`helper` supports any argument signature valid in a Ruby method definition: positional, keyword, default values, and blocks.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# No arguments
|
|
132
|
+
helper(:project_root) { loki_up.parent.to_s }
|
|
133
|
+
|
|
134
|
+
# Positional arguments
|
|
135
|
+
helper(:gem_path) { |name| "lib/#{name}/version.rb" }
|
|
136
|
+
|
|
137
|
+
# Positional with default
|
|
138
|
+
helper(:tag_prefix) { |sep = "-"| "#{@@project}#{sep}" }
|
|
139
|
+
|
|
140
|
+
# Positional and keyword arguments
|
|
141
|
+
helper(:format_version) { |name, version, prefix: "v", separator: "-"|
|
|
142
|
+
"#{prefix}#{name}#{separator}#{version}"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Wrong argument counts or unknown keyword names raise the same `ArgumentError` Ruby raises for any method call — no special error handling needed.
|
|
147
|
+
|
|
148
|
+
### Visibility
|
|
149
|
+
|
|
150
|
+
`helper`-defined methods are:
|
|
151
|
+
|
|
152
|
+
- **Excluded from `asgard help`** — they never appear as commands
|
|
153
|
+
- **Blocked from CLI invocation** — cannot be called directly from the command line
|
|
154
|
+
- **Private on the instance side** — not accessible from outside the class
|
|
155
|
+
|
|
156
|
+
### When to use `helper`
|
|
157
|
+
|
|
158
|
+
Use `helper` when the value or computation must be available in both a class-level DSL call (`header`, `footer`, a `@@var` initializer) and inside task instance methods. For helpers that are only needed inside task bodies, a plain `private` method is simpler.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
78
162
|
## Choosing Between `private` and `no_commands`
|
|
79
163
|
|
|
80
164
|
| | `private` | `no_commands` |
|
data/docs/options.md
CHANGED
|
@@ -71,19 +71,53 @@ end
|
|
|
71
71
|
|
|
72
72
|
Both `deploy` and `migrate` automatically accept `--dry-run` and `--env`.
|
|
73
73
|
|
|
74
|
+
### Boolean class options and `no_negate`
|
|
75
|
+
|
|
76
|
+
Thor automatically generates `[--no-name]` and `[--skip-name]` help entries alongside every boolean `class_option`. When negation is meaningful — `--dry-run` paired with `--no-dry-run` — this is useful. When negation is meaningless, call `no_negate` immediately after the declaration to remove the extra variants from the help output:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
class Tasks
|
|
80
|
+
class_option :color,
|
|
81
|
+
type: :boolean,
|
|
82
|
+
default: true,
|
|
83
|
+
desc: "Colorise output"
|
|
84
|
+
no_negate :color
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Help output before `no_negate`:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
[--color], [--no-color], [--skip-color] # Colorise output
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Help output after `no_negate`:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
[--color] # Colorise output
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`no_negate` accepts multiple option names in a single call:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
no_negate :color, :version, :emoji
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
It has no effect on runtime behaviour — `--no-color` still works on the CLI; only the help display is affected.
|
|
107
|
+
|
|
74
108
|
---
|
|
75
109
|
|
|
76
110
|
## Built-in Flags
|
|
77
111
|
|
|
78
|
-
`Tasks` ships with three built-in
|
|
112
|
+
`Tasks` ships with three built-in `class_option` declarations — `--debug`, `--verbose`, and `--version` — all visible in the Options section of `asgard help`.
|
|
79
113
|
|
|
80
114
|
### `--version`
|
|
81
115
|
|
|
82
|
-
Prints `Asgard::VERSION` and exits.
|
|
116
|
+
A `class_option :version` of type `:boolean`. Prints `Asgard::VERSION` and exits. Handled by `Asgard.run!` before the `.loki` file is loaded, so it works even in a directory without a `.loki` file. `no_negate :version` suppresses the `[--no-version]` / `[--skip-version]` variants (see [Boolean class options and `no_negate`](#boolean-class-options-and-no_negate) above):
|
|
83
117
|
|
|
84
118
|
```bash
|
|
85
119
|
asgard --version
|
|
86
|
-
# 0.
|
|
120
|
+
# 0.3.0
|
|
87
121
|
```
|
|
88
122
|
|
|
89
123
|
### `--debug`
|
|
@@ -173,8 +207,8 @@ asgard deploy production --verbose
|
|
|
173
207
|
Methods whose names start with `_` are considered gem-owned in Asgard's naming convention. `run!` guards against invoking them directly from the CLI:
|
|
174
208
|
|
|
175
209
|
```bash
|
|
176
|
-
asgard
|
|
177
|
-
# asgard: unknown command '
|
|
210
|
+
asgard _something
|
|
211
|
+
# asgard: unknown command '_something'
|
|
178
212
|
```
|
|
179
213
|
|
|
180
|
-
If you define your own methods on `Tasks`, avoid the `_` prefix to prevent them from being
|
|
214
|
+
If you define your own methods on `Tasks`, avoid the `_` prefix to prevent them from being blocked. Built-in `class_option` declarations (like `--version`, `--debug`, `--verbose`) do not use the `_` prefix because they are options, not commands.
|
data/docs/task-files.md
CHANGED
|
@@ -93,7 +93,7 @@ import("gem_tasks.loki") ? "loaded now" : "already loaded"
|
|
|
93
93
|
|
|
94
94
|
## Finding Files with `loki_up`
|
|
95
95
|
|
|
96
|
-
`loki_up(name = ".loki")` searches `Dir.pwd` and each ancestor directory for a file with the given name, returning
|
|
96
|
+
`loki_up(name = ".loki")` searches `Dir.pwd` and each ancestor directory for a file with the given name, returning a `Pathname` or `nil`. It does **not** load the file — it only finds it.
|
|
97
97
|
|
|
98
98
|
Despite the name, `loki_up` is not limited to `.loki` files — it will locate any file by name. This makes it useful for finding shared config files, `.env` files, or any other resource that lives somewhere up the directory tree:
|
|
99
99
|
|
|
@@ -401,7 +401,7 @@ end
|
|
|
401
401
|
|
|
402
402
|
| Method | Finds? | Loads? | Glob? | Ancestor search? |
|
|
403
403
|
|---|---|---|---|---|
|
|
404
|
-
| `loki_up(name)` | Yes | No | No | Yes |
|
|
404
|
+
| `loki_up(name)` | Yes (`Pathname`) | No | No | Yes |
|
|
405
405
|
| `import(path)` | No | Yes | Yes | No |
|
|
406
406
|
| `import_up(name)` | Yes | Yes | Yes | Yes |
|
|
407
407
|
| Asgard's `run!` | Yes | `.loki` only | No | Yes |
|
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# Demonstrates Thor subcommands registered on the top-level Tasks class.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Port state is persisted to .server.port so stop and restart always
|
|
5
|
+
# operate on the same port the server was started on.
|
|
6
6
|
#
|
|
7
7
|
# Usage:
|
|
8
|
-
# asgard server
|
|
8
|
+
# asgard server # shows subcommand help
|
|
9
9
|
# asgard server start
|
|
10
|
-
# asgard server start
|
|
10
|
+
# asgard server start -p 8000
|
|
11
|
+
# asgard server start -p 4000 --workers 4 --daemon
|
|
12
|
+
# asgard server stop
|
|
11
13
|
# asgard server stop --force
|
|
12
14
|
# asgard server status
|
|
13
|
-
# asgard server restart
|
|
15
|
+
# asgard server restart # stops then starts on the persisted port
|
|
16
|
+
|
|
17
|
+
SERVER_PORT_FILE = "tmp/.server.port".freeze
|
|
14
18
|
|
|
15
19
|
class ServerCommands < Tasks
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
default_task :help # This is the default value for the default_task
|
|
21
|
+
|
|
22
|
+
helper(:server_port) {
|
|
23
|
+
File.exist?(SERVER_PORT_FILE) ? File.read(SERVER_PORT_FILE).strip.to_i : 3000
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
desc "Start the server"
|
|
27
|
+
option :port, aliases: "-p", type: :numeric, default: 3000, desc: "Port to listen on"
|
|
28
|
+
option :daemon, aliases: "-d", type: :boolean, default: false, desc: "Run as a background daemon"
|
|
29
|
+
option :workers, aliases: "-w", type: :numeric, default: 2, desc: "Number of worker processes"
|
|
19
30
|
option :log, type: :string, default: "log/server.log",
|
|
20
31
|
banner: "FILE", desc: "Write logs to FILE"
|
|
21
|
-
def start
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
options[:daemon] ? " (daemon)" : ""
|
|
26
|
-
]
|
|
32
|
+
def start
|
|
33
|
+
FileUtils.mkdir_p("tmp")
|
|
34
|
+
File.write(SERVER_PORT_FILE, options[:port].to_s)
|
|
35
|
+
puts "Starting server on :#{options[:port]} with #{options[:workers]} workers#{options[:daemon] ? " (daemon)" : ""}..."
|
|
27
36
|
end
|
|
28
37
|
|
|
29
38
|
desc "Stop the running server"
|
|
@@ -31,22 +40,22 @@ class ServerCommands < Tasks
|
|
|
31
40
|
option :wait, type: :numeric, default: 30, desc: "Seconds to wait for shutdown"
|
|
32
41
|
def stop
|
|
33
42
|
if options[:force]
|
|
34
|
-
puts "Force-stopping server..."
|
|
43
|
+
puts "Force-stopping server on :#{server_port}..."
|
|
35
44
|
else
|
|
36
|
-
puts "Gracefully stopping server (timeout: #{options[:wait]}s)..."
|
|
45
|
+
puts "Gracefully stopping server on :#{server_port} (timeout: #{options[:wait] || 30}s)..."
|
|
37
46
|
end
|
|
38
47
|
end
|
|
39
48
|
|
|
40
49
|
desc "Show server status and process info"
|
|
41
50
|
def status
|
|
42
|
-
puts "
|
|
51
|
+
puts "Server is listening on :#{server_port}"
|
|
43
52
|
end
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
puts "Server restarted
|
|
54
|
+
depends_on :stop
|
|
55
|
+
desc "Restart the server on the same port it was started on"
|
|
56
|
+
def restart
|
|
57
|
+
puts "Starting server on :#{server_port}..."
|
|
58
|
+
puts "Server restarted."
|
|
50
59
|
end
|
|
51
60
|
end
|
|
52
61
|
|
data/gem_tasks.loki
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Gem lifecycle tasks — imported by .loki
|
|
3
|
+
|
|
4
|
+
class Tasks
|
|
5
|
+
desc "Build the gem package"
|
|
6
|
+
depends_on :quality
|
|
7
|
+
def build
|
|
8
|
+
sh "mkdir -p pkg"
|
|
9
|
+
sh "gem build asgard.gemspec"
|
|
10
|
+
sh "mv asgard-#{project_version}.gem pkg/"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc "Build and install gem locally"
|
|
14
|
+
depends_on :build
|
|
15
|
+
def install
|
|
16
|
+
sh "gem install pkg/asgard-#{project_version}.gem"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc "Release to RubyGems"
|
|
20
|
+
depends_on :quality
|
|
21
|
+
def release
|
|
22
|
+
tag = "v#{project_version}"
|
|
23
|
+
gem_file = "pkg/asgard-#{project_version}.gem"
|
|
24
|
+
|
|
25
|
+
abort "Working directory is not clean — commit or stash changes first." unless `git status --porcelain`.strip.empty?
|
|
26
|
+
abort "Tag #{tag} already exists." unless `git tag -l #{tag}`.strip.empty?
|
|
27
|
+
|
|
28
|
+
sh "mkdir -p pkg"
|
|
29
|
+
sh "gem build asgard.gemspec"
|
|
30
|
+
sh "mv asgard-#{project_version}.gem pkg/"
|
|
31
|
+
sh "git tag #{tag}"
|
|
32
|
+
sh "git push origin #{tag}"
|
|
33
|
+
sh "gem push #{gem_file}"
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/asgard/base.rb
CHANGED
|
@@ -92,17 +92,46 @@ module Asgard
|
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
|
|
95
|
+
# Suppress [--no-name] / [--skip-name] from help for boolean class options
|
|
96
|
+
# where negation is meaningless. Call after class_option declarations.
|
|
97
|
+
def no_negate(*names)
|
|
98
|
+
names.each do |name|
|
|
99
|
+
opt = class_options[name]
|
|
100
|
+
next unless opt
|
|
101
|
+
opt.define_singleton_method(:usage) do |padding = 0|
|
|
102
|
+
aliases_for_usage.ljust(padding) + "[#{switch_name}]"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def header(text = nil)
|
|
108
|
+
return @_header if text.nil?
|
|
109
|
+
(@_header ||= []) << text
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def footer(text = nil)
|
|
113
|
+
return @_footer if text.nil?
|
|
114
|
+
(@_footer ||= []).unshift(text)
|
|
115
|
+
end
|
|
116
|
+
|
|
95
117
|
def dotenv(path = ".env")
|
|
96
118
|
require "dotenv"
|
|
97
119
|
Dotenv.load(path) if File.exist?(path)
|
|
98
120
|
end
|
|
99
121
|
|
|
122
|
+
def helper(name, &)
|
|
123
|
+
define_singleton_method(name, &)
|
|
124
|
+
no_commands { private define_method(name) { |*args, **kwargs, &blk| self.class.send(name, *args, **kwargs, &blk) } }
|
|
125
|
+
end
|
|
126
|
+
|
|
100
127
|
def default_task(meth = nil)
|
|
101
128
|
if meth && meth != :none && @_default_task_location
|
|
102
129
|
here = caller_locations(1, 1).first
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
130
|
+
# rubocop:disable Style/StderrPuts -- warn bypasses $stderr in Ruby 4.0, breaking capture_io in tests
|
|
131
|
+
$stderr.puts "asgard: default_task :#{meth} at #{here.path}:#{here.lineno} " \
|
|
132
|
+
"overrides default_task :#{@_default_task_name} set at " \
|
|
133
|
+
"#{@_default_task_location.path}:#{@_default_task_location.lineno}"
|
|
134
|
+
# rubocop:enable Style/StderrPuts
|
|
106
135
|
end
|
|
107
136
|
if meth && meth != :none
|
|
108
137
|
@_default_task_location = caller_locations(1, 1).first
|
|
@@ -184,6 +213,13 @@ module Asgard
|
|
|
184
213
|
end
|
|
185
214
|
end
|
|
186
215
|
|
|
216
|
+
def help(command = nil, subcommand = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
217
|
+
say self.class.header.join("\n\n") if self.class.header && command.nil?
|
|
218
|
+
say "\n"
|
|
219
|
+
super
|
|
220
|
+
say self.class.footer.join("\n\n") if self.class.footer && command.nil?
|
|
221
|
+
end
|
|
222
|
+
|
|
187
223
|
no_commands do
|
|
188
224
|
# Dispatch hook: resolves and runs all deps (in parallel where declared)
|
|
189
225
|
# before executing the target command.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
3
5
|
module Kernel
|
|
4
6
|
def debug? = $DEBUG
|
|
5
7
|
def verbose? = $VERBOSE
|
|
@@ -15,11 +17,11 @@ module Kernel
|
|
|
15
17
|
module_function :env
|
|
16
18
|
|
|
17
19
|
def loki_up(name = ".loki")
|
|
18
|
-
dir = Dir.pwd
|
|
20
|
+
dir = Pathname.new(Dir.pwd)
|
|
19
21
|
loop do
|
|
20
|
-
candidate =
|
|
21
|
-
return candidate if
|
|
22
|
-
parent =
|
|
22
|
+
candidate = dir + name
|
|
23
|
+
return candidate if candidate.exist?
|
|
24
|
+
parent = dir.parent
|
|
23
25
|
break if parent == dir
|
|
24
26
|
dir = parent
|
|
25
27
|
end
|
data/lib/asgard/tasks.rb
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
# It is pre-defined by the gem so .loki files never need to declare a class.
|
|
5
5
|
# Auxiliary *.loki files define modules which are imported into Tasks.
|
|
6
6
|
class Tasks < Asgard::Base
|
|
7
|
+
header "\nasgard v#{Asgard::VERSION} The Mighty Thor and Loki working for you"
|
|
8
|
+
|
|
9
|
+
footer <<~FOOT
|
|
10
|
+
\nDocumentation ... https://madbomber.github.io/asgard
|
|
11
|
+
Github Repo ..... https://github.com/MadBomber/asgard\n
|
|
12
|
+
FOOT
|
|
13
|
+
|
|
7
14
|
class_option :debug,
|
|
8
15
|
type: :boolean,
|
|
9
16
|
default: false,
|
|
@@ -14,10 +21,9 @@ class Tasks < Asgard::Base
|
|
|
14
21
|
default: false,
|
|
15
22
|
desc: "Enable verbose output ($VERBOSE = true)"
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
end
|
|
24
|
+
class_option :version,
|
|
25
|
+
type: :boolean,
|
|
26
|
+
default: false,
|
|
27
|
+
desc: "Show asgard version and exit"
|
|
28
|
+
no_negate :version
|
|
23
29
|
end
|
data/lib/asgard/version.rb
CHANGED
data/lib/asgard.rb
CHANGED
|
@@ -19,6 +19,10 @@ module Asgard
|
|
|
19
19
|
# Main entry point invoked by the asgard executable.
|
|
20
20
|
def self.run!(argv)
|
|
21
21
|
abort "asgard: unknown command '#{argv.first}'" if argv.first&.start_with?("_")
|
|
22
|
+
if argv.include?("--version")
|
|
23
|
+
puts Asgard::VERSION
|
|
24
|
+
exit
|
|
25
|
+
end
|
|
22
26
|
task_file = find_task_file or abort "asgard: no .loki file found in #{Dir.pwd}"
|
|
23
27
|
before = Asgard::Base.subclasses.dup
|
|
24
28
|
load task_file
|
data/quality.loki
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Quality gate tasks — imported by .loki
|
|
3
|
+
|
|
4
|
+
class Tasks
|
|
5
|
+
desc "Run the test suite"
|
|
6
|
+
def test
|
|
7
|
+
output = `bundle exec ruby -Ilib:test test/test_asgard.rb 2>&1`
|
|
8
|
+
@test_result = $?.success? ? :pass : :fail
|
|
9
|
+
if @test_result == :fail
|
|
10
|
+
lines = output.lines
|
|
11
|
+
start = lines.index { |l| l =~ /^\s+1\) / } || 0
|
|
12
|
+
finish = lines.rindex { |l| l =~ /\d+ runs,/ } || -1
|
|
13
|
+
print lines[start..finish].join
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "Run all quality gates (tests, RuboCop, Flog) in parallel"
|
|
18
|
+
depends_on [:test, :rubocop, :flog_check]
|
|
19
|
+
def quality
|
|
20
|
+
results = { tests: @test_result, rubocop: @rubocop_result, flog: @flog_result }
|
|
21
|
+
|
|
22
|
+
if @flog_failures&.any?
|
|
23
|
+
puts "\nFlog failures (must be refactored):"
|
|
24
|
+
@flog_failures.each { |v| puts " #{v}" }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
puts "\n#{"=" * 60}"
|
|
28
|
+
puts "Quality Summary"
|
|
29
|
+
puts "=" * 60
|
|
30
|
+
results.each { |gate, status| puts " [#{status == :pass ? "PASS" : "FAIL"}] #{gate}" }
|
|
31
|
+
puts "=" * 60
|
|
32
|
+
|
|
33
|
+
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
34
|
+
puts "\nAll quality gates passed."
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "Check code style with RuboCop"
|
|
38
|
+
def rubocop
|
|
39
|
+
output = `RUBOCOP_CACHE_ROOT=tmp/rubocop_cache bundle exec rubocop 2>&1`
|
|
40
|
+
@rubocop_result = $?.success? ? :pass : :fail
|
|
41
|
+
if @rubocop_result == :fail
|
|
42
|
+
output.each_line { |l| print l if l =~ /:\d+:\d+: [CWEF]: / }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "Auto-correct RuboCop offenses"
|
|
47
|
+
def rubocop_fix
|
|
48
|
+
sh "RUBOCOP_CACHE_ROOT=tmp/rubocop_cache bundle exec rubocop -a"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "Check code complexity with Flog (warn >=20, fail >=50)"
|
|
52
|
+
def flog_check
|
|
53
|
+
require "flog"
|
|
54
|
+
|
|
55
|
+
method_warn = 20.0
|
|
56
|
+
method_fail = 50.0
|
|
57
|
+
|
|
58
|
+
flogger = Flog.new(all: true)
|
|
59
|
+
flogger.flog(*Dir.glob("lib/**/*.rb"))
|
|
60
|
+
|
|
61
|
+
warnings = []
|
|
62
|
+
@flog_failures = []
|
|
63
|
+
|
|
64
|
+
flogger.each_by_score do |method_name, score|
|
|
65
|
+
next if method_name.end_with?("#none")
|
|
66
|
+
if score > method_fail
|
|
67
|
+
@flog_failures << "#{"%.1f" % score}: #{method_name}"
|
|
68
|
+
elsif score > method_warn
|
|
69
|
+
warnings << "#{"%.1f" % score}: #{method_name}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@flog_result = @flog_failures.empty? ? :pass : :fail
|
|
74
|
+
end
|
|
75
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: asgard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -71,7 +71,6 @@ files:
|
|
|
71
71
|
- COMMITS.md
|
|
72
72
|
- LICENSE.txt
|
|
73
73
|
- README.md
|
|
74
|
-
- Rakefile
|
|
75
74
|
- bin/asgard
|
|
76
75
|
- bin/console
|
|
77
76
|
- bin/setup
|
|
@@ -101,6 +100,7 @@ files:
|
|
|
101
100
|
- examples/subdir/.loki
|
|
102
101
|
- examples/subdir/import_demo.loki
|
|
103
102
|
- examples/subdir/import_up_demo.loki
|
|
103
|
+
- gem_tasks.loki
|
|
104
104
|
- lib/asgard.rb
|
|
105
105
|
- lib/asgard/base.rb
|
|
106
106
|
- lib/asgard/kernel_methods.rb
|
|
@@ -108,6 +108,7 @@ files:
|
|
|
108
108
|
- lib/asgard/tasks.rb
|
|
109
109
|
- lib/asgard/version.rb
|
|
110
110
|
- mkdocs.yml
|
|
111
|
+
- quality.loki
|
|
111
112
|
- sig/asgard.rbs
|
|
112
113
|
homepage: https://github.com/madbomber/asgard
|
|
113
114
|
licenses:
|
data/Rakefile
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/gem_tasks"
|
|
4
|
-
require "minitest/test_task"
|
|
5
|
-
|
|
6
|
-
SIMPLECOV_PRELUDE = <<~RUBY
|
|
7
|
-
require "simplecov"
|
|
8
|
-
SimpleCov.start do
|
|
9
|
-
add_filter "/test/"
|
|
10
|
-
minimum_coverage 95
|
|
11
|
-
end
|
|
12
|
-
RUBY
|
|
13
|
-
|
|
14
|
-
Minitest::TestTask.create do |t|
|
|
15
|
-
t.test_prelude = SIMPLECOV_PRELUDE
|
|
16
|
-
end
|
|
17
|
-
|
|
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"
|
|
25
|
-
end
|
|
26
|
-
|
|
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
|