asgard 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.loki +7 -1
- data/.reek.yml +42 -0
- data/CHANGELOG.md +60 -4
- data/CLAUDE.md +8 -2
- data/README.md +77 -2
- data/doc_tasks.loki +21 -0
- data/docs/api.md +28 -5
- data/docs/changelog.md +1 -132
- data/docs/dependencies.md +18 -7
- data/docs/index.md +1 -0
- data/docs/schedule.md +101 -0
- data/docs/shell.md +30 -1
- data/docs/tasks.md +40 -0
- data/examples/depends_on_block/bad/.loki +51 -0
- data/examples/depends_on_block/good/.loki +83 -0
- data/examples/kitchen_sink.loki +13 -0
- data/lib/asgard/base/dependency_graph.rb +43 -7
- data/lib/asgard/base/task_dsl.rb +1 -2
- data/lib/asgard/errors.rb +7 -0
- data/lib/asgard/schedule/commands.rb +174 -0
- data/lib/asgard/schedule/declaration.rb +176 -0
- data/lib/asgard/schedule/launchd.rb +183 -0
- data/lib/asgard/schedule/systemd.rb +199 -0
- data/lib/asgard/schedule.rb +44 -0
- data/lib/asgard/shell.rb +21 -8
- data/lib/asgard/tasks.rb +12 -0
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +9 -4
- data/mkdocs.yml +1 -0
- data/quality.loki +7 -2
- data/quality_rails.loki +3 -2
- metadata +14 -3
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "etc"
|
|
5
|
+
|
|
6
|
+
module Asgard
|
|
7
|
+
module Schedule
|
|
8
|
+
# Linux backend: each declaration becomes a systemd user service + timer
|
|
9
|
+
# pair (~/.config/systemd/user/asgard.<project>.<name>.{service,timer}).
|
|
10
|
+
# Calendar timers are Persistent=, so a run missed while the machine was
|
|
11
|
+
# off or asleep happens on the next boot/wake. Requires systemd 240+ (for
|
|
12
|
+
# StandardOutput=append:). User timers only run while you are logged in
|
|
13
|
+
# unless lingering is enabled (`loginctl enable-linger`); #notes says so.
|
|
14
|
+
# Implements the backend API documented in declaration.rb.
|
|
15
|
+
class Systemd
|
|
16
|
+
UNIT_PREFIX = "asgard"
|
|
17
|
+
DAY_ABBREVIATIONS = %w[Sun Mon Tue Wed Thu Fri Sat].freeze
|
|
18
|
+
|
|
19
|
+
# ---- pure helpers -----------------------------------------------------
|
|
20
|
+
|
|
21
|
+
def self.unit_prefix(project) = "#{UNIT_PREFIX}.#{Schedule.slug(project)}."
|
|
22
|
+
|
|
23
|
+
def self.unit(project, name) = "#{unit_prefix(project)}#{name}"
|
|
24
|
+
|
|
25
|
+
# { hour: 17, minute: 30, days: [1, 5] } => "Mon,Fri *-*-* 17:30:00"
|
|
26
|
+
def self.on_calendar(entry)
|
|
27
|
+
days = entry[:days]&.map { |day| DAY_ABBREVIATIONS.fetch(day) }&.join(",")
|
|
28
|
+
[days, format("*-*-* %<hour>02d:%<minute>02d:00", entry)].compact.join(" ")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# systemd expands %specifiers everywhere; % must be doubled to be literal.
|
|
32
|
+
def self.escape_specifiers(text) = text.to_s.gsub("%", "%%")
|
|
33
|
+
|
|
34
|
+
# A double-quoted word for ExecStart=/Environment=. $ is doubled too so
|
|
35
|
+
# ExecStart= doesn't expand it as a variable.
|
|
36
|
+
def self.quote(text)
|
|
37
|
+
%("#{escape_specifiers(text).gsub('\\') { '\\\\' }.gsub('"', '\\"').gsub('$', '$$')}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.service_unit(description:, arguments:, working_directory:, environment:, log_path:)
|
|
41
|
+
env_lines = environment.map { |key, value| "Environment=#{quote("#{key}=#{value}")}" }
|
|
42
|
+
log = escape_specifiers(log_path)
|
|
43
|
+
<<~UNIT
|
|
44
|
+
[Unit]
|
|
45
|
+
Description=#{escape_specifiers(description)}
|
|
46
|
+
|
|
47
|
+
[Service]
|
|
48
|
+
Type=oneshot
|
|
49
|
+
WorkingDirectory=#{escape_specifiers(working_directory)}
|
|
50
|
+
#{env_lines.join("\n")}
|
|
51
|
+
ExecStart=#{arguments.map { |arg| quote(arg) }.join(' ')}
|
|
52
|
+
StandardOutput=append:#{log}
|
|
53
|
+
StandardError=append:#{log}
|
|
54
|
+
UNIT
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# every: runs one interval after the timer starts, then one interval
|
|
58
|
+
# after each run, matching launchd's StartInterval.
|
|
59
|
+
def self.timer_unit(description:, service:, calendar: nil, every: nil)
|
|
60
|
+
schedule = if every
|
|
61
|
+
["OnActiveSec=#{every}", "OnUnitActiveSec=#{every}"]
|
|
62
|
+
else
|
|
63
|
+
[*calendar.map { |entry| "OnCalendar=#{on_calendar(entry)}" }, "Persistent=true"]
|
|
64
|
+
end
|
|
65
|
+
<<~UNIT
|
|
66
|
+
[Unit]
|
|
67
|
+
Description=#{escape_specifiers(description)}
|
|
68
|
+
|
|
69
|
+
[Timer]
|
|
70
|
+
#{schedule.join("\n")}
|
|
71
|
+
AccuracySec=1s
|
|
72
|
+
Unit=#{service}
|
|
73
|
+
|
|
74
|
+
[Install]
|
|
75
|
+
WantedBy=timers.target
|
|
76
|
+
UNIT
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# `systemctl show -p A -p B` output => { "A" => "...", "B" => "..." }
|
|
80
|
+
def self.parse_show(output) = output.lines.to_h { |line| line.chomp.split("=", 2) }.reject { |k, _| k.to_s.empty? }
|
|
81
|
+
|
|
82
|
+
# ---- backend API ------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def initialize(project:, root:, home: Dir.home, runner: Schedule.runner, env: ENV, user: Etc.getpwuid(Process.uid).name)
|
|
85
|
+
@project = project
|
|
86
|
+
@root = root
|
|
87
|
+
@runner = runner
|
|
88
|
+
@user = user
|
|
89
|
+
@config = env["XDG_CONFIG_HOME"] || File.join(home, ".config")
|
|
90
|
+
@state = env["XDG_STATE_HOME"] || File.join(home, ".local", "state")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def scheduler = "systemd"
|
|
94
|
+
|
|
95
|
+
def unit(name) = self.class.unit(@project, name)
|
|
96
|
+
|
|
97
|
+
def service_path(name) = File.join(@config, "systemd", "user", "#{unit(name)}.service")
|
|
98
|
+
|
|
99
|
+
def timer_path(name) = File.join(@config, "systemd", "user", "#{unit(name)}.timer")
|
|
100
|
+
|
|
101
|
+
def log_path(name) = File.join(@state, "asgard", "#{unit(name)}.log")
|
|
102
|
+
|
|
103
|
+
def files(spec, asgard:, direnv:)
|
|
104
|
+
name, task, args, every = spec.values_at(:name, :task, :args, :every)
|
|
105
|
+
klass = self.class
|
|
106
|
+
description = "#{Schedule.command_line(task, args)} (#{@project})"
|
|
107
|
+
service = klass.service_unit(
|
|
108
|
+
description:,
|
|
109
|
+
arguments: Schedule.program_arguments(task, args, root: @root, asgard:, direnv:),
|
|
110
|
+
working_directory: @root,
|
|
111
|
+
environment: Schedule.environment(spec),
|
|
112
|
+
log_path: log_path(name)
|
|
113
|
+
)
|
|
114
|
+
timer = klass.timer_unit(
|
|
115
|
+
description:,
|
|
116
|
+
service: "#{unit(name)}.service",
|
|
117
|
+
calendar: every ? nil : Schedule.calendar(at: spec[:at], on: spec[:on]),
|
|
118
|
+
every:
|
|
119
|
+
)
|
|
120
|
+
{ service_path(name) => service, timer_path(name) => timer }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A timer that exists but is disabled was stopped on purpose; keep it so.
|
|
124
|
+
def install(spec, asgard:, direnv:)
|
|
125
|
+
name = spec[:name]
|
|
126
|
+
timer_file = timer_path(name)
|
|
127
|
+
timer_unit = timer(name)
|
|
128
|
+
stopped = File.exist?(timer_file) && !enabled?(name)
|
|
129
|
+
FileUtils.mkdir_p [File.dirname(timer_file), File.dirname(log_path(name))]
|
|
130
|
+
files(spec, asgard:, direnv:).each { |path, content| File.write(path, content) }
|
|
131
|
+
systemctl! "daemon-reload"
|
|
132
|
+
return :stopped if stopped
|
|
133
|
+
|
|
134
|
+
systemctl! "enable", timer_unit
|
|
135
|
+
systemctl! "restart", timer_unit # picks up a changed schedule
|
|
136
|
+
:active
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def uninstall(name)
|
|
140
|
+
service_unit = service(name)
|
|
141
|
+
systemctl "disable", "--now", timer(name)
|
|
142
|
+
systemctl "stop", service_unit
|
|
143
|
+
FileUtils.rm_f [service_path(name), timer_path(name)]
|
|
144
|
+
systemctl "daemon-reload"
|
|
145
|
+
systemctl "reset-failed", service_unit
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def start(name) = systemctl!("enable", "--now", timer(name))
|
|
149
|
+
|
|
150
|
+
def stop(name) = systemctl!("disable", "--now", timer(name))
|
|
151
|
+
|
|
152
|
+
def trigger(name) = systemctl!("start", "--no-block", service(name))
|
|
153
|
+
|
|
154
|
+
def installed_names
|
|
155
|
+
prefix = self.class.unit_prefix(@project)
|
|
156
|
+
Dir.glob(timer_path("*")).map { |path| File.basename(path, ".timer").delete_prefix(prefix) }.sort
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def status(name)
|
|
160
|
+
_, active = systemctl("is-active", "--quiet", timer(name))
|
|
161
|
+
state = if active then :active
|
|
162
|
+
elsif enabled?(name) then :not_loaded
|
|
163
|
+
else :stopped
|
|
164
|
+
end
|
|
165
|
+
out, = systemctl("show", service(name), "-p", "ExecMainStatus", "-p", "ExecMainExitTimestampMonotonic")
|
|
166
|
+
props = self.class.parse_show(out)
|
|
167
|
+
stamp = props["ExecMainExitTimestampMonotonic"].to_s
|
|
168
|
+
ran = !stamp.empty? && stamp != "0"
|
|
169
|
+
{ state:, last_exit: ran ? props["ExecMainStatus"] : nil }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def notes
|
|
173
|
+
out, ok = run("loginctl", "show-user", @user.to_s, "-p", "Linger", "--value")
|
|
174
|
+
return [] unless ok && out.strip == "no"
|
|
175
|
+
|
|
176
|
+
["systemd: user timers only run while #{@user} is logged in; run `loginctl enable-linger` to keep them running."]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def timer(name) = "#{unit(name)}.timer"
|
|
182
|
+
|
|
183
|
+
def service(name) = "#{unit(name)}.service"
|
|
184
|
+
|
|
185
|
+
def enabled?(name) = systemctl("is-enabled", "--quiet", timer(name)).last
|
|
186
|
+
|
|
187
|
+
def systemctl(*) = run("systemctl", "--user", *)
|
|
188
|
+
|
|
189
|
+
def systemctl!(*args)
|
|
190
|
+
out, ok = systemctl(*args)
|
|
191
|
+
raise Error, "systemctl --user #{args.join(' ')} failed: #{out.strip}" unless ok
|
|
192
|
+
|
|
193
|
+
out
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def run(*argv) = @runner.call(*argv)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "schedule/declaration"
|
|
4
|
+
require_relative "schedule/launchd"
|
|
5
|
+
require_relative "schedule/systemd"
|
|
6
|
+
require_relative "schedule/commands"
|
|
7
|
+
|
|
8
|
+
module Asgard
|
|
9
|
+
# Runs asgard tasks periodically under the platform's own scheduler.
|
|
10
|
+
# Declare entries at class level in a .loki file:
|
|
11
|
+
#
|
|
12
|
+
# class Tasks
|
|
13
|
+
# schedule :daily_summary, at: "17:30", on: :weekdays
|
|
14
|
+
# schedule :sync, every: 3600
|
|
15
|
+
# schedule :report, options: "--period week", at: "16:00", on: :friday
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# then manage them with `asgard schedule install|list|start|stop|...`.
|
|
19
|
+
module Schedule
|
|
20
|
+
class << self
|
|
21
|
+
# Declared entries for this run, keyed by entry name.
|
|
22
|
+
def declarations
|
|
23
|
+
@declarations ||= {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Records one declaration. Redeclaring an identical entry is a no-op
|
|
27
|
+
# (a .loki file loaded twice); a different entry under the same name
|
|
28
|
+
# is an error.
|
|
29
|
+
def declare(task, **settings)
|
|
30
|
+
spec = normalize(task, **settings)
|
|
31
|
+
name = spec[:name]
|
|
32
|
+
taken = declarations[name]
|
|
33
|
+
raise ArgumentError, "schedule: #{name.inspect} is already declared; give one a distinct as: name" if taken && taken != spec
|
|
34
|
+
|
|
35
|
+
declarations[name] = spec
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The class-level `schedule` declaration helper for Tasks.
|
|
40
|
+
module DSL
|
|
41
|
+
def schedule(task, **) = Schedule.declare(task, **)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/asgard/shell.rb
CHANGED
|
@@ -8,17 +8,21 @@ module Asgard
|
|
|
8
8
|
# Run a shell script. Multiline strings are passed to bash -c; single-line
|
|
9
9
|
# strings are passed to system directly. Exits with the command's status
|
|
10
10
|
# code on failure.
|
|
11
|
-
|
|
11
|
+
#
|
|
12
|
+
# Pass exec: true to replace the current process instead of forking —
|
|
13
|
+
# useful for a task's final, long-running command (e.g. a dev server)
|
|
14
|
+
# so the asgard/ruby process doesn't sit resident in memory alongside it.
|
|
15
|
+
def sh(script, silent: false, exec: false)
|
|
12
16
|
script = script.strip
|
|
13
17
|
$stdout.puts script unless silent
|
|
18
|
+
argv = shell_argv(script)
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
exit($CHILD_STATUS.exitstatus) unless success
|
|
20
|
+
if exec
|
|
21
|
+
$stdout.flush
|
|
22
|
+
Kernel.exec(*argv)
|
|
23
|
+
else
|
|
24
|
+
exit($CHILD_STATUS.exitstatus) unless system(*argv)
|
|
25
|
+
end
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
# Write +script+ to a tempfile and execute it with +interpreter+.
|
|
@@ -42,5 +46,14 @@ module Asgard
|
|
|
42
46
|
exit($CHILD_STATUS.exitstatus) unless $CHILD_STATUS.success?
|
|
43
47
|
end
|
|
44
48
|
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# The argv passed to system/exec for +script+: multi-line scripts run
|
|
53
|
+
# through `bash -c` so assignments carry across lines; single-line
|
|
54
|
+
# scripts run directly.
|
|
55
|
+
def shell_argv(script)
|
|
56
|
+
script.include?("\n") ? ["bash", "-c", script] : [script]
|
|
57
|
+
end
|
|
45
58
|
end
|
|
46
59
|
end
|
data/lib/asgard/tasks.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "schedule"
|
|
4
|
+
|
|
3
5
|
# Tasks is the single conventional entry point for all .loki files.
|
|
4
6
|
# It is pre-defined by the gem so .loki files never need to declare a class.
|
|
5
7
|
# Auxiliary *.loki files define modules which are imported into Tasks.
|
|
@@ -32,4 +34,14 @@ class Tasks < Asgard::Base
|
|
|
32
34
|
default: false,
|
|
33
35
|
desc: "Diagnose .loki resolution, imports, and task definitions for the CWD, then exit"
|
|
34
36
|
no_negate :doctor
|
|
37
|
+
|
|
38
|
+
# Class-level `schedule :task, at: "HH:MM"` declarations (see Asgard::Schedule).
|
|
39
|
+
extend Asgard::Schedule::DSL
|
|
40
|
+
|
|
41
|
+
# Gem-owned, so the command is _schedule; the map keeps `asgard schedule`
|
|
42
|
+
# as the name users type, and ancestor_name keeps it in subcommand help.
|
|
43
|
+
desc "schedule SUBCOMMAND", "Manage schedules declared with `schedule :task, ...` (launchd on macOS, systemd on Linux)"
|
|
44
|
+
subcommand "_schedule", Asgard::Schedule::Commands
|
|
45
|
+
map "schedule" => :_schedule
|
|
46
|
+
Asgard::Schedule::Commands.commands.each_value { |command| command.ancestor_name = "schedule" }
|
|
35
47
|
end
|
data/lib/asgard/version.rb
CHANGED
data/lib/asgard.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "asgard/errors"
|
|
3
4
|
require_relative "asgard/version"
|
|
4
5
|
require_relative "asgard/kernel_methods"
|
|
5
6
|
require_relative "asgard/shell"
|
|
@@ -8,9 +9,6 @@ require_relative "asgard/tasks"
|
|
|
8
9
|
require_relative "asgard/doctor"
|
|
9
10
|
|
|
10
11
|
module Asgard
|
|
11
|
-
class Error < StandardError; end
|
|
12
|
-
class CircularDependencyError < Error; end
|
|
13
|
-
|
|
14
12
|
# Search the current directory and its ancestors for a .loki task file.
|
|
15
13
|
# Returns the path string, or nil if not found.
|
|
16
14
|
def self.find_task_file
|
|
@@ -35,10 +33,17 @@ module Asgard
|
|
|
35
33
|
newly_defined = Asgard::Base.subclasses - before
|
|
36
34
|
(newly_defined + [Tasks]).uniq.each(&:validate_deps!)
|
|
37
35
|
Tasks._reset_ran!
|
|
38
|
-
Tasks.start(argv)
|
|
36
|
+
result = Tasks.start(argv)
|
|
37
|
+
# Quality-gate convention: a task signals failure by returning :fail
|
|
38
|
+
# (see dev/quality.loki's *_check tasks). Surface that as a nonzero
|
|
39
|
+
# exit so callers (CI, cross-repo runners) can rely on $?.
|
|
40
|
+
exit(1) if result == :fail
|
|
41
|
+
result
|
|
39
42
|
rescue CircularDependencyError => e
|
|
40
43
|
abort "asgard: circular dependency — #{e.message}"
|
|
41
44
|
rescue Error => e
|
|
42
45
|
abort "asgard: #{e.message}"
|
|
46
|
+
rescue Interrupt
|
|
47
|
+
exit(130)
|
|
43
48
|
end
|
|
44
49
|
end
|
data/mkdocs.yml
CHANGED
data/quality.loki
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# Quality gate tasks — imported by .loki
|
|
3
3
|
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
4
6
|
class Tasks
|
|
7
|
+
# One process for every test/test_*.rb, so SimpleCov sees the whole suite.
|
|
8
|
+
TEST_LOADER = %q(Dir["test/test_*.rb"].each { |f| require File.expand_path(f) }).shellescape.freeze
|
|
9
|
+
|
|
5
10
|
desc "Run the test suite"
|
|
6
11
|
def test_check
|
|
7
|
-
output = `bundle exec ruby -Ilib:test
|
|
12
|
+
output = `bundle exec ruby -Ilib:test -e #{TEST_LOADER} 2>&1`
|
|
8
13
|
result = $?.success? ? :pass : :fail
|
|
9
14
|
File.write("test_output.txt", output)
|
|
10
15
|
summary = output.lines.reverse.find { |l| l =~ /\d+ runs,/ }&.strip || result.to_s
|
|
@@ -14,7 +19,7 @@ class Tasks
|
|
|
14
19
|
|
|
15
20
|
desc "Run the test suite with verbose output"
|
|
16
21
|
def test_verbose
|
|
17
|
-
sh "bundle exec ruby -Ilib:test
|
|
22
|
+
sh "bundle exec ruby -Ilib:test -e #{TEST_LOADER} -- -v"
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
desc "Run every *_check quality gate task in parallel"
|
data/quality_rails.loki
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
# Rails-specific quality gate tasks — imported by .loki only when
|
|
3
|
-
#
|
|
2
|
+
# Rails-specific quality gate tasks — imported by .loki only when RAILS_ROOT
|
|
3
|
+
# is set. asgard runs as a separate process outside the app it's checking,
|
|
4
|
+
# so `defined?(Rails)` never sees the app's Rails constant.
|
|
4
5
|
#
|
|
5
6
|
# `quality` (in quality.loki) discovers every *_check task at run time by
|
|
6
7
|
# introspecting Tasks.all_commands, rather than a fixed depends_on list — so
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -40,7 +40,8 @@ dependencies:
|
|
|
40
40
|
description: |
|
|
41
41
|
A powerful Ruby-based task runner for any kind of project with task dependency tracking
|
|
42
42
|
and concurrent execution of designated tasks. Uses Thor for its rich CLI options, var
|
|
43
|
-
declarations, dotenv, sh/shebang helpers,
|
|
43
|
+
declarations, dotenv, sh/shebang helpers, importable task files, and scheduled
|
|
44
|
+
execution of tasks via launchd (macOS) or systemd timers (Linux).
|
|
44
45
|
email:
|
|
45
46
|
- dewayne@vanhoozer.me
|
|
46
47
|
executables:
|
|
@@ -62,6 +63,7 @@ files:
|
|
|
62
63
|
- bin/asgard
|
|
63
64
|
- bin/console
|
|
64
65
|
- bin/setup
|
|
66
|
+
- doc_tasks.loki
|
|
65
67
|
- docs/api.md
|
|
66
68
|
- docs/assets/css/custom.css
|
|
67
69
|
- docs/assets/images/asgard.jpg
|
|
@@ -73,6 +75,7 @@ files:
|
|
|
73
75
|
- docs/helpers.md
|
|
74
76
|
- docs/index.md
|
|
75
77
|
- docs/options.md
|
|
78
|
+
- docs/schedule.md
|
|
76
79
|
- docs/shell.md
|
|
77
80
|
- docs/subcommands.md
|
|
78
81
|
- docs/task-files.md
|
|
@@ -83,6 +86,8 @@ files:
|
|
|
83
86
|
- examples/bad.loki
|
|
84
87
|
- examples/concurrent.loki
|
|
85
88
|
- examples/db_subcommands.loki
|
|
89
|
+
- examples/depends_on_block/bad/.loki
|
|
90
|
+
- examples/depends_on_block/good/.loki
|
|
86
91
|
- examples/env_usage.loki
|
|
87
92
|
- examples/kitchen_sink.loki
|
|
88
93
|
- examples/server_subcommands.loki
|
|
@@ -100,7 +105,13 @@ files:
|
|
|
100
105
|
- lib/asgard/doctor.rb
|
|
101
106
|
- lib/asgard/doctor/report.rb
|
|
102
107
|
- lib/asgard/doctor/task_sections.rb
|
|
108
|
+
- lib/asgard/errors.rb
|
|
103
109
|
- lib/asgard/kernel_methods.rb
|
|
110
|
+
- lib/asgard/schedule.rb
|
|
111
|
+
- lib/asgard/schedule/commands.rb
|
|
112
|
+
- lib/asgard/schedule/declaration.rb
|
|
113
|
+
- lib/asgard/schedule/launchd.rb
|
|
114
|
+
- lib/asgard/schedule/systemd.rb
|
|
104
115
|
- lib/asgard/shell.rb
|
|
105
116
|
- lib/asgard/tasks.rb
|
|
106
117
|
- lib/asgard/version.rb
|
|
@@ -131,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
131
142
|
- !ruby/object:Gem::Version
|
|
132
143
|
version: '0'
|
|
133
144
|
requirements: []
|
|
134
|
-
rubygems_version: 4.0.
|
|
145
|
+
rubygems_version: 4.0.21
|
|
135
146
|
specification_version: 4
|
|
136
147
|
summary: A powerful Ruby-based task runner
|
|
137
148
|
test_files: []
|