asgard 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.loki +13 -26
- data/.rubocop.yml +157 -0
- data/CHANGELOG.md +89 -11
- data/CLAUDE.md +19 -9
- data/README.md +161 -54
- data/docs/api.md +91 -15
- data/docs/changelog.md +12 -0
- data/docs/dependencies.md +25 -25
- data/docs/environment.md +30 -14
- data/docs/examples.md +3 -3
- data/docs/getting-started.md +5 -6
- data/docs/helpers.md +117 -9
- data/docs/index.md +6 -6
- data/docs/options.md +42 -8
- data/docs/shell.md +11 -11
- data/docs/subcommands.md +9 -9
- data/docs/task-files.md +266 -113
- data/docs/tasks.md +17 -15
- data/docs/variables.md +267 -51
- data/examples/.env +4 -0
- data/examples/.loki +24 -2
- data/examples/concurrent.loki +5 -5
- data/examples/db_subcommands.loki +3 -3
- data/examples/env_usage.loki +27 -0
- data/examples/kitchen_sink.loki +48 -15
- data/examples/server_subcommands.loki +33 -24
- data/examples/subdir/.loki +12 -0
- data/examples/subdir/import_demo.loki +14 -0
- data/examples/subdir/import_up_demo.loki +18 -0
- data/gem_tasks.loki +35 -0
- data/lib/asgard/base.rb +158 -80
- data/lib/asgard/kernel_methods.rb +79 -0
- data/lib/asgard/shell.rb +8 -7
- data/lib/asgard/tasks.rb +12 -17
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +6 -18
- data/quality.loki +75 -0
- metadata +15 -5
- data/Rakefile +0 -22
data/examples/kitchen_sink.loki
CHANGED
|
@@ -5,9 +5,20 @@
|
|
|
5
5
|
# install, release) so this file can be loaded alongside them without conflict.
|
|
6
6
|
|
|
7
7
|
class Tasks
|
|
8
|
-
# ──
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# ── Ruby class variables — shared across all tasks and subcommands ──────────
|
|
9
|
+
@@app_name ||= "my_app".freeze
|
|
10
|
+
@@build_dir ||= "builds/#{@@app_name}".freeze
|
|
11
|
+
|
|
12
|
+
# ── Computed values — plain private methods replace the removed var DSL ──────
|
|
13
|
+
# Declare as private so Thor does not try to register them as commands.
|
|
14
|
+
# Use @ivar ||= inside the method body to memoize expensive calls.
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def version = `git describe --tags --always`.strip
|
|
18
|
+
def sha = `git rev-parse --short HEAD`.strip
|
|
19
|
+
def branch = @branch ||= `git rev-parse --abbrev-ref HEAD`.strip
|
|
20
|
+
|
|
21
|
+
public
|
|
11
22
|
|
|
12
23
|
# ── Asgard: dotenv — load environment variables ────────────────────────────
|
|
13
24
|
# Uncomment to activate:
|
|
@@ -37,9 +48,9 @@ class Tasks
|
|
|
37
48
|
map "pl" => :pipeline
|
|
38
49
|
|
|
39
50
|
# ── Basic task — no parameters ─────────────────────────────────────────────
|
|
40
|
-
desc "
|
|
51
|
+
desc "Say hello (default task when no command is given)"
|
|
41
52
|
def greet
|
|
42
|
-
puts "Hello from #{app_name} (#{options[:env]}
|
|
53
|
+
puts "Hello from #{@@app_name} #{version} (#{branch}) in #{options[:env]} mode!"
|
|
43
54
|
end
|
|
44
55
|
|
|
45
56
|
# ── Positional parameter with default ──────────────────────────────────────
|
|
@@ -72,14 +83,14 @@ class Tasks
|
|
|
72
83
|
end
|
|
73
84
|
|
|
74
85
|
# ── method_option — all five option types ──────────────────────────────────
|
|
75
|
-
desc "
|
|
86
|
+
desc "Compile the project"
|
|
76
87
|
option :output, aliases: "-o", type: :string, default: "dist/", desc: "Output directory"
|
|
77
88
|
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Enable verbose output"
|
|
78
89
|
option :jobs, aliases: "-j", type: :numeric, default: 1, desc: "Number of parallel jobs"
|
|
79
90
|
option :tags, type: :array, desc: "Build tags to apply"
|
|
80
91
|
option :defines, type: :hash, desc: "Preprocessor defines (KEY:VALUE)"
|
|
81
92
|
def compile
|
|
82
|
-
puts "Compiling #{app_name} → #{options[:output]}"
|
|
93
|
+
puts "Compiling #{@@app_name} → #{options[:output]}"
|
|
83
94
|
end
|
|
84
95
|
|
|
85
96
|
# ── required option + enum + banner ────────────────────────────────────────
|
|
@@ -99,7 +110,7 @@ class Tasks
|
|
|
99
110
|
default: "main",
|
|
100
111
|
desc: "Git branch to deploy"
|
|
101
112
|
def deploy(env = "staging")
|
|
102
|
-
puts "Deploying #{app_name}@#{options[:branch]} to #{env}..."
|
|
113
|
+
puts "Deploying #{@@app_name}@#{options[:branch]} to #{env}..."
|
|
103
114
|
end
|
|
104
115
|
|
|
105
116
|
# ── long_desc — extended help shown by `asgard help report` ────────────────
|
|
@@ -115,7 +126,7 @@ class Tasks
|
|
|
115
126
|
asgard report --format json --output report.json\x5
|
|
116
127
|
asgard rp --format text
|
|
117
128
|
LONGDESC
|
|
118
|
-
desc "
|
|
129
|
+
desc "Generate a project report"
|
|
119
130
|
option :format, type: :string, default: "text", enum: %w[text html json], desc: "Output format"
|
|
120
131
|
option :since, type: :string, banner: "DATE", desc: "Limit to changes after DATE"
|
|
121
132
|
option :output, type: :string, banner: "FILE", desc: "Write output to FILE"
|
|
@@ -124,30 +135,52 @@ class Tasks
|
|
|
124
135
|
end
|
|
125
136
|
|
|
126
137
|
# ── Asgard depends_on: sequential — analyze runs before spec ───────────────
|
|
127
|
-
desc "
|
|
138
|
+
desc "Check code style and complexity"
|
|
128
139
|
def analyze = puts "Analyzing..."
|
|
129
140
|
|
|
130
141
|
depends_on :analyze
|
|
131
|
-
desc "
|
|
142
|
+
desc "Run the test suite (depends on: analyze)"
|
|
132
143
|
def spec = puts "Running specs..."
|
|
133
144
|
|
|
134
145
|
# ── Asgard depends_on: parallel — analyze and typecheck run concurrently ───
|
|
135
|
-
desc "
|
|
146
|
+
desc "Run the type checker"
|
|
136
147
|
def typecheck = puts "Type checking..."
|
|
137
148
|
|
|
138
149
|
depends_on [:analyze, :typecheck]
|
|
139
|
-
desc "
|
|
150
|
+
desc "Run analyze and typecheck in parallel"
|
|
140
151
|
def check = puts "All checks passed."
|
|
141
152
|
|
|
142
153
|
# ── Asgard depends_on: mixed sequential + parallel ─────────────────────────
|
|
143
|
-
desc "
|
|
154
|
+
desc "Create distribution archive"
|
|
144
155
|
def pack = puts "Packing..."
|
|
145
156
|
|
|
146
157
|
# check → compile+spec (parallel) → pack → pipeline
|
|
147
158
|
depends_on :check, [:compile, :spec], :pack
|
|
148
|
-
desc "
|
|
159
|
+
desc "Full pipeline: check → compile+spec → pack"
|
|
149
160
|
def pipeline = puts "Pipeline complete."
|
|
150
161
|
|
|
162
|
+
# ── Asgard: debug? / verbose? — Kernel predicates for conditional output ──────
|
|
163
|
+
# debug? returns true when --debug is passed (sets $DEBUG)
|
|
164
|
+
# verbose? returns true when --verbose is passed (sets $VERBOSE)
|
|
165
|
+
# Both are set by Asgard before invoke_command runs, so they are safe
|
|
166
|
+
# to read inside any task body.
|
|
167
|
+
#
|
|
168
|
+
# Run with:
|
|
169
|
+
# asgard status --verbose
|
|
170
|
+
# asgard status --debug
|
|
171
|
+
# asgard status --verbose --debug
|
|
172
|
+
desc "Show application status"
|
|
173
|
+
def status
|
|
174
|
+
puts "#{@@app_name} is running in #{options[:env]} mode."
|
|
175
|
+
puts " build dir : #{@@build_dir}" if verbose?
|
|
176
|
+
puts " sha : #{current_sha}" if verbose?
|
|
177
|
+
if debug?
|
|
178
|
+
puts " $DEBUG : #{$DEBUG.inspect}"
|
|
179
|
+
puts " $VERBOSE : #{$VERBOSE.inspect}"
|
|
180
|
+
puts " options : #{options.inspect}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
151
184
|
# ── Thor: no_commands — public helper excluded from CLI and --help ──────────
|
|
152
185
|
no_commands do
|
|
153
186
|
def current_sha
|
|
@@ -1,52 +1,61 @@
|
|
|
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
|
-
desc "
|
|
38
|
+
desc "Stop the running server"
|
|
30
39
|
option :force, aliases: "-f", type: :boolean, default: false, desc: "Force-kill without draining"
|
|
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
|
-
desc "
|
|
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
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# examples/subdir/.loki — root task file for this subdirectory.
|
|
2
|
+
#
|
|
3
|
+
# When asgard is run from examples/subdir/, it loads this file.
|
|
4
|
+
# From here, import pulls in sibling task files explicitly.
|
|
5
|
+
#
|
|
6
|
+
# import accepts a direct path or a glob:
|
|
7
|
+
# import "import_up_demo.loki" # a single file
|
|
8
|
+
# import "*.loki" # all loki files in this directory
|
|
9
|
+
# import "tasks/**/*.loki" # all loki files under a tasks/ tree
|
|
10
|
+
# import "../shared/*.loki" # files in a sibling directory
|
|
11
|
+
|
|
12
|
+
import "import_up_demo.loki"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# This file lives in a subdirectory and is explicitly imported by the
|
|
3
|
+
# parent examples/.loki via import "subdir/import_demo.loki".
|
|
4
|
+
#
|
|
5
|
+
# In a real project a subdirectory might hold task files scoped to a
|
|
6
|
+
# specific concern (deploy, database, CI) while the root .loki wires
|
|
7
|
+
# them all together with import.
|
|
8
|
+
|
|
9
|
+
class Tasks
|
|
10
|
+
desc "Confirm this task was loaded from a subdirectory via import"
|
|
11
|
+
def subdir_task
|
|
12
|
+
puts "Loaded from examples/subdir/import_demo.loki"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Demonstrates import_up — find a file by walking up the directory tree
|
|
3
|
+
# and load it, without needing to know how deep the current file is.
|
|
4
|
+
#
|
|
5
|
+
# import_up(name) combines loki_up(name) + import(path) in one call.
|
|
6
|
+
# It searches CWD, then each ancestor in turn, and loads the first match.
|
|
7
|
+
#
|
|
8
|
+
# This is useful when a nested task file needs to pull in something from
|
|
9
|
+
# the project root — a shared config, a common helpers file — without
|
|
10
|
+
# hardcoding a relative path that would break if the file moves.
|
|
11
|
+
#
|
|
12
|
+
# import_up also accepts a glob:
|
|
13
|
+
# import_up "*.loki" # first directory (walking up) that contains any .loki file
|
|
14
|
+
# import_up "config/settings.loki" # first ancestor with config/settings.loki
|
|
15
|
+
# import_up ".env" # locate a .env file anywhere up the tree
|
|
16
|
+
|
|
17
|
+
# Load env_usage.loki from the nearest ancestor directory that contains it.
|
|
18
|
+
import_up "env_usage.loki"
|
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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
|
-
require "set"
|
|
5
4
|
require "dagwood"
|
|
6
5
|
|
|
7
6
|
module Asgard
|
|
@@ -16,9 +15,10 @@ module Asgard
|
|
|
16
15
|
def inherited(subclass)
|
|
17
16
|
super
|
|
18
17
|
Asgard::Base.subclasses << subclass
|
|
19
|
-
subclass.instance_variable_set(:@_deps,
|
|
20
|
-
subclass.instance_variable_set(:@
|
|
21
|
-
subclass.instance_variable_set(:@
|
|
18
|
+
subclass.instance_variable_set(:@_deps, {})
|
|
19
|
+
subclass.instance_variable_set(:@_pending_deps, [])
|
|
20
|
+
subclass.instance_variable_set(:@_pending_single_desc, nil)
|
|
21
|
+
subclass.instance_variable_set(:@_pending_single_desc_opts, nil)
|
|
22
22
|
subclass.instance_variable_set(:@_running, Set.new)
|
|
23
23
|
subclass.instance_variable_set(:@_done, Set.new)
|
|
24
24
|
subclass.instance_variable_set(:@_cond, Hash.new { |h, k| h[k] = ConditionVariable.new })
|
|
@@ -29,10 +29,6 @@ module Asgard
|
|
|
29
29
|
@_deps ||= {}
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def _vars
|
|
33
|
-
@_vars ||= {}
|
|
34
|
-
end
|
|
35
|
-
|
|
36
32
|
def _running
|
|
37
33
|
@_running ||= Set.new
|
|
38
34
|
end
|
|
@@ -65,7 +61,7 @@ module Asgard
|
|
|
65
61
|
def _build_dep_graph(stages)
|
|
66
62
|
graph = {}
|
|
67
63
|
stages.each_with_index do |stage, i|
|
|
68
|
-
prev_stage = i
|
|
64
|
+
prev_stage = i.positive? ? stages[i - 1] : []
|
|
69
65
|
stage.each { |task| graph[task] = prev_stage.dup }
|
|
70
66
|
end
|
|
71
67
|
graph
|
|
@@ -82,23 +78,40 @@ module Asgard
|
|
|
82
78
|
@_pending_deps = tasks
|
|
83
79
|
end
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
81
|
+
# Allow single-argument desc: desc "Run the tests"
|
|
82
|
+
# The usage string defaults to the method name when the description is the only arg.
|
|
83
|
+
def desc(usage_or_desc, description = nil, options = {})
|
|
84
|
+
if description.nil? || description.is_a?(Hash)
|
|
85
|
+
options = description if description.is_a?(Hash)
|
|
86
|
+
@_pending_single_desc = usage_or_desc
|
|
87
|
+
@_pending_single_desc_opts = options
|
|
88
|
+
else
|
|
89
|
+
@_pending_single_desc = nil
|
|
90
|
+
@_pending_single_desc_opts = nil
|
|
91
|
+
super
|
|
92
|
+
end
|
|
93
|
+
end
|
|
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}]"
|
|
96
103
|
end
|
|
97
104
|
end
|
|
98
105
|
end
|
|
99
106
|
|
|
100
|
-
def
|
|
101
|
-
|
|
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)
|
|
102
115
|
end
|
|
103
116
|
|
|
104
117
|
def dotenv(path = ".env")
|
|
@@ -106,44 +119,86 @@ module Asgard
|
|
|
106
119
|
Dotenv.load(path) if File.exist?(path)
|
|
107
120
|
end
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
|
|
127
|
+
def default_task(meth = nil)
|
|
128
|
+
if meth && meth != :none && @_default_task_location
|
|
129
|
+
here = caller_locations(1, 1).first
|
|
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
|
|
115
135
|
end
|
|
136
|
+
if meth && meth != :none
|
|
137
|
+
@_default_task_location = caller_locations(1, 1).first
|
|
138
|
+
@_default_task_name = meth
|
|
139
|
+
end
|
|
140
|
+
super
|
|
141
|
+
end
|
|
116
142
|
|
|
143
|
+
# Validate the full dep graph for cycles using Dagwood::DependencyGraph.
|
|
144
|
+
def validate_deps!
|
|
145
|
+
_check_orphaned_deps!
|
|
117
146
|
return if _deps.empty?
|
|
118
147
|
|
|
119
148
|
all_task_names = all_commands.keys.map(&:to_sym)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
149
|
+
_check_undefined_deps!(all_task_names)
|
|
150
|
+
_check_dep_arities!
|
|
151
|
+
_build_and_sort_graph(all_task_names)
|
|
152
|
+
rescue TSort::Cyclic => e
|
|
153
|
+
raise Asgard::CircularDependencyError, e.message
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
123
157
|
|
|
158
|
+
def _check_orphaned_deps!
|
|
159
|
+
pending = Array(@_pending_deps)
|
|
160
|
+
return unless pending.any?
|
|
161
|
+
|
|
162
|
+
raise Asgard::Error,
|
|
163
|
+
"depends_on(#{pending.join(', ')}) declared without a following task definition"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def _check_undefined_deps!(all_task_names)
|
|
124
167
|
undefined = _deps.values.flatten.uniq - all_task_names
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
168
|
+
return unless undefined.any?
|
|
169
|
+
|
|
170
|
+
raise Asgard::Error, "undefined task(s) in depends_on: #{undefined.sort.join(', ')}"
|
|
171
|
+
end
|
|
128
172
|
|
|
129
|
-
|
|
173
|
+
def _check_dep_arities!
|
|
174
|
+
_deps.each_value do |stages|
|
|
130
175
|
stages.flatten.each do |dep|
|
|
131
|
-
meth = instance_method(dep.to_s)
|
|
132
|
-
next unless meth
|
|
176
|
+
meth = instance_method(dep.to_s)
|
|
133
177
|
required = meth.parameters.count { |type, _| type == :req }
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
178
|
+
next unless required.positive?
|
|
179
|
+
|
|
180
|
+
raise Asgard::Error,
|
|
181
|
+
"task '#{dep}' has #{required} required argument(s) and cannot be used as a dependency"
|
|
138
182
|
end
|
|
139
183
|
end
|
|
184
|
+
end
|
|
140
185
|
|
|
186
|
+
def _build_and_sort_graph(all_task_names)
|
|
187
|
+
full_graph = all_task_names.to_h { |task| [task, _deps.fetch(task, []).flatten] }
|
|
141
188
|
Dagwood::DependencyGraph.new(full_graph).order
|
|
142
|
-
rescue TSort::Cyclic => e
|
|
143
|
-
raise Asgard::CircularDependencyError, e.message
|
|
144
189
|
end
|
|
145
190
|
|
|
191
|
+
public
|
|
192
|
+
|
|
146
193
|
def method_added(method_name)
|
|
194
|
+
if @_pending_single_desc && !no_commands?
|
|
195
|
+
pending_desc = @_pending_single_desc
|
|
196
|
+
pending_opts = @_pending_single_desc_opts || {}
|
|
197
|
+
@_pending_single_desc = nil
|
|
198
|
+
@_pending_single_desc_opts = nil
|
|
199
|
+
desc(method_name.to_s, pending_desc, pending_opts)
|
|
200
|
+
end
|
|
201
|
+
|
|
147
202
|
return super unless @usage
|
|
148
203
|
|
|
149
204
|
pending = Array(@_pending_deps).dup
|
|
@@ -158,6 +213,13 @@ module Asgard
|
|
|
158
213
|
end
|
|
159
214
|
end
|
|
160
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
|
+
|
|
161
223
|
no_commands do
|
|
162
224
|
# Dispatch hook: resolves and runs all deps (in parallel where declared)
|
|
163
225
|
# before executing the target command.
|
|
@@ -171,51 +233,67 @@ module Asgard
|
|
|
171
233
|
$DEBUG = true if options[:debug]
|
|
172
234
|
$VERBOSE = true if options[:verbose]
|
|
173
235
|
target = command.name.to_sym
|
|
174
|
-
|
|
175
|
-
should_run = self.class._ran_mutex.synchronize do
|
|
176
|
-
if self.class._done.include?(target)
|
|
177
|
-
false
|
|
178
|
-
elsif self.class._running.include?(target)
|
|
179
|
-
self.class._cond[target].wait(self.class._ran_mutex) until self.class._done.include?(target)
|
|
180
|
-
false
|
|
181
|
-
else
|
|
182
|
-
self.class._running.add(target)
|
|
183
|
-
true
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
return unless should_run
|
|
236
|
+
return unless acquire_run_token(target)
|
|
187
237
|
|
|
188
238
|
begin
|
|
189
|
-
|
|
190
|
-
if stages&.any?
|
|
191
|
-
graph = self.class._build_dep_graph(stages)
|
|
192
|
-
groups = Dagwood::DependencyGraph.new(graph).parallel_order
|
|
193
|
-
|
|
194
|
-
groups.each do |group|
|
|
195
|
-
if group.size > 1
|
|
196
|
-
threads = group.map { |task| Thread.new { _run_dep(task) } }
|
|
197
|
-
errors = []
|
|
198
|
-
threads.each { |t| begin; t.join; rescue => e; errors << e; end }
|
|
199
|
-
raise errors.first if errors.any?
|
|
200
|
-
else
|
|
201
|
-
_run_dep(group.first)
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
239
|
+
run_deps_for(target)
|
|
206
240
|
command.run(self, *args)
|
|
207
241
|
ensure
|
|
208
|
-
|
|
209
|
-
self.class._done.add(target)
|
|
210
|
-
self.class._cond[target].broadcast
|
|
211
|
-
end
|
|
242
|
+
signal_done(target)
|
|
212
243
|
end
|
|
213
244
|
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
private
|
|
248
|
+
|
|
249
|
+
def acquire_run_token(target)
|
|
250
|
+
self.class._ran_mutex.synchronize do
|
|
251
|
+
if self.class._done.include?(target)
|
|
252
|
+
false
|
|
253
|
+
elsif self.class._running.include?(target)
|
|
254
|
+
self.class._cond[target].wait(self.class._ran_mutex) until self.class._done.include?(target)
|
|
255
|
+
false
|
|
256
|
+
else
|
|
257
|
+
self.class._running.add(target)
|
|
258
|
+
true
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def run_deps_for(target)
|
|
264
|
+
stages = self.class._deps[target]
|
|
265
|
+
return unless stages&.any?
|
|
266
|
+
|
|
267
|
+
groups = Dagwood::DependencyGraph.new(self.class._build_dep_graph(stages)).parallel_order
|
|
268
|
+
groups.each { |group| run_dep_group(group) }
|
|
269
|
+
end
|
|
214
270
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
271
|
+
def run_dep_group(group)
|
|
272
|
+
if group.size > 1
|
|
273
|
+
threads = group.map { |task| Thread.new { run_dep(task) } }
|
|
274
|
+
errors = []
|
|
275
|
+
threads.each { |t| begin; t.join; rescue => e; errors << e; end }
|
|
276
|
+
if errors.size == 1
|
|
277
|
+
raise errors.first
|
|
278
|
+
elsif errors.any?
|
|
279
|
+
errors.each { |e| warn "asgard: #{e.message}" }
|
|
280
|
+
raise Asgard::Error, "#{errors.size} parallel dependencies failed"
|
|
281
|
+
end
|
|
282
|
+
else
|
|
283
|
+
run_dep(group.first)
|
|
218
284
|
end
|
|
219
285
|
end
|
|
286
|
+
|
|
287
|
+
def signal_done(target)
|
|
288
|
+
self.class._ran_mutex.synchronize do
|
|
289
|
+
self.class._done.add(target)
|
|
290
|
+
self.class._cond[target].broadcast
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def run_dep(task)
|
|
295
|
+
command = self.class.all_commands[task.to_s]
|
|
296
|
+
invoke_command(command) if command
|
|
297
|
+
end
|
|
220
298
|
end
|
|
221
299
|
end
|