pups 1.0.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 +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +126 -0
- data/Rakefile +6 -0
- data/bin/pups +9 -0
- data/lib/pups.rb +23 -0
- data/lib/pups/cli.rb +36 -0
- data/lib/pups/command.rb +17 -0
- data/lib/pups/config.rb +115 -0
- data/lib/pups/exec_command.rb +116 -0
- data/lib/pups/file_command.rb +37 -0
- data/lib/pups/merge_command.rb +48 -0
- data/lib/pups/replace_command.rb +43 -0
- data/lib/pups/runit.rb +40 -0
- data/lib/pups/version.rb +3 -0
- data/pups.gemspec +26 -0
- data/test/config_test.rb +55 -0
- data/test/exec_command_test.rb +113 -0
- data/test/file_command_test.rb +27 -0
- data/test/merge_command_test.rb +58 -0
- data/test/replace_command_test.rb +112 -0
- data/test/test_helper.rb +4 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 055f645c7b240abd8d9d8206029043d1097611c8
|
4
|
+
data.tar.gz: 9a0c6803b35b3813058ed89c33f937a959dc9fcb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2b64563d45db0ec09bb16465e09d81ca4ef39ac92dee8c1b6dd894a24ea4eda5ecc08e6890391e57e3653365f29a39fdcf17399ee383b7bcb9e7e5978ec44b52
|
7
|
+
data.tar.gz: 0069a7d13b2070240fc23b17befc9dc2a34d1a51c6e7573b575acf892d22251fbf1bd4c44a94820cc86c3d1dbdb5999e0b048fec6dc528a2ce44f0a81d62f9f7
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Sam Saffron
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# pups
|
2
|
+
|
3
|
+
Simple yaml based bootstrapper
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'pups'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install pups
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
pups is a small library that allows you to automate the process of creating Unix images.
|
22
|
+
|
23
|
+
Example:
|
24
|
+
|
25
|
+
```
|
26
|
+
# somefile.yaml
|
27
|
+
params:
|
28
|
+
hello: hello world
|
29
|
+
|
30
|
+
run:
|
31
|
+
exec: /bin/bash -c 'echo $hello >>> hello'
|
32
|
+
```
|
33
|
+
|
34
|
+
Running: `pups somefile.yaml` will execute the shell script resulting in a file called "hello" with the "hello world" contents
|
35
|
+
|
36
|
+
### Features:
|
37
|
+
|
38
|
+
####Execution
|
39
|
+
|
40
|
+
Run multiple commands in one path:
|
41
|
+
|
42
|
+
```
|
43
|
+
run:
|
44
|
+
exec:
|
45
|
+
cd: some/path
|
46
|
+
cmd:
|
47
|
+
- echo 1
|
48
|
+
- echo 2
|
49
|
+
```
|
50
|
+
|
51
|
+
Run commands in the background (for services etc)
|
52
|
+
|
53
|
+
```
|
54
|
+
run:
|
55
|
+
exec:
|
56
|
+
cmd: /usr/bin/sshd
|
57
|
+
background: true
|
58
|
+
```
|
59
|
+
|
60
|
+
Suppress exceptions on certain commands
|
61
|
+
|
62
|
+
```
|
63
|
+
run:
|
64
|
+
exec:
|
65
|
+
cmd: /test
|
66
|
+
raise_on_faile: false
|
67
|
+
```
|
68
|
+
|
69
|
+
####Replacements:
|
70
|
+
|
71
|
+
```
|
72
|
+
run:
|
73
|
+
replace:
|
74
|
+
filename: "/etc/redis/redis.conf"
|
75
|
+
from: /^pidfile.*$/
|
76
|
+
to: ""
|
77
|
+
```
|
78
|
+
|
79
|
+
Will substitued the regex with blank, removing the pidfile line
|
80
|
+
|
81
|
+
```
|
82
|
+
run:
|
83
|
+
- replace:
|
84
|
+
filename: "/etc/nginx/conf.d/discourse.conf"
|
85
|
+
from: /upstream[^\}]+\}/m
|
86
|
+
to: "upstream discourse {
|
87
|
+
server 127.0.0.1:3000;
|
88
|
+
}"
|
89
|
+
```
|
90
|
+
|
91
|
+
Multiline replace using regex
|
92
|
+
|
93
|
+
####Merge yaml files:
|
94
|
+
|
95
|
+
```
|
96
|
+
home: /var/www/my_app
|
97
|
+
params:
|
98
|
+
database_yml:
|
99
|
+
production:
|
100
|
+
username: discourse
|
101
|
+
password: foo
|
102
|
+
|
103
|
+
run:
|
104
|
+
- merge: $home/config/database.yml $database_yml
|
105
|
+
|
106
|
+
```
|
107
|
+
|
108
|
+
Will merge the yaml file with the inline contents
|
109
|
+
|
110
|
+
####A common environment
|
111
|
+
|
112
|
+
```
|
113
|
+
env:
|
114
|
+
MY_ENV: 1
|
115
|
+
```
|
116
|
+
|
117
|
+
All executions will get this environment set up
|
118
|
+
|
119
|
+
|
120
|
+
## Contributing
|
121
|
+
|
122
|
+
1. Fork it
|
123
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
124
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
125
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
126
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/pups
ADDED
data/lib/pups.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
require "pups/version"
|
5
|
+
require "pups/config"
|
6
|
+
require "pups/command"
|
7
|
+
require "pups/exec_command"
|
8
|
+
require "pups/merge_command"
|
9
|
+
require "pups/replace_command"
|
10
|
+
require "pups/file_command"
|
11
|
+
|
12
|
+
require "pups/runit"
|
13
|
+
|
14
|
+
module Pups
|
15
|
+
def self.log
|
16
|
+
# at the moment docker likes this
|
17
|
+
@logger ||= Logger.new(STDERR)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.log=(logger)
|
21
|
+
@logger = logger
|
22
|
+
end
|
23
|
+
end
|
data/lib/pups/cli.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class Pups::Cli
|
2
|
+
|
3
|
+
def self.usage
|
4
|
+
puts "Usage: pups FILE or pups --stdin"
|
5
|
+
exit 1
|
6
|
+
end
|
7
|
+
def self.run(args)
|
8
|
+
if args.length != 1
|
9
|
+
usage
|
10
|
+
end
|
11
|
+
|
12
|
+
Pups.log.info("Loading #{args[0]}")
|
13
|
+
if args[0] == "--stdin"
|
14
|
+
conf = STDIN.readlines.join
|
15
|
+
split = conf.split("_FILE_SEPERATOR_")
|
16
|
+
|
17
|
+
conf = nil
|
18
|
+
split.each do |data|
|
19
|
+
current = YAML.load(data.strip)
|
20
|
+
if conf
|
21
|
+
conf = Pups::MergeCommand.deep_merge(conf, current, :merge_arrays)
|
22
|
+
else
|
23
|
+
conf = current
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
config = Pups::Config.new(conf)
|
28
|
+
else
|
29
|
+
config = Pups::Config.load_file(args[0])
|
30
|
+
end
|
31
|
+
config.run
|
32
|
+
|
33
|
+
ensure
|
34
|
+
Pups::ExecCommand.terminate_async
|
35
|
+
end
|
36
|
+
end
|
data/lib/pups/command.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Pups::Command
|
2
|
+
|
3
|
+
def self.run(command,params)
|
4
|
+
case command
|
5
|
+
when String then self.from_str(command,params).run
|
6
|
+
when Hash then self.from_hash(command,params).run
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.interpolate_params(cmd,params)
|
11
|
+
Pups::Config.interpolate_params(cmd,params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def interpolate_params(cmd)
|
15
|
+
Pups::Command.interpolate_params(cmd,@params)
|
16
|
+
end
|
17
|
+
end
|
data/lib/pups/config.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
class Pups::Config
|
2
|
+
|
3
|
+
attr_reader :config, :params
|
4
|
+
|
5
|
+
def self.load_file(config_file)
|
6
|
+
new YAML.load_file(config_file)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.load_config(config)
|
10
|
+
new YAML.load(config)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
validate!(@config)
|
16
|
+
@params = @config["params"]
|
17
|
+
@params ||= {}
|
18
|
+
ENV.each do |k,v|
|
19
|
+
@params["$ENV_#{k}"] = v
|
20
|
+
end
|
21
|
+
inject_hooks
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate!(conf)
|
25
|
+
# raise proper errors if nodes are missing etc
|
26
|
+
end
|
27
|
+
|
28
|
+
def inject_hooks
|
29
|
+
return unless hooks = @config["hooks"]
|
30
|
+
|
31
|
+
run = @config["run"]
|
32
|
+
|
33
|
+
positions = {}
|
34
|
+
run.each do |row|
|
35
|
+
if Hash === row
|
36
|
+
command = row.first
|
37
|
+
if Hash === command[1]
|
38
|
+
hook = command[1]["hook"]
|
39
|
+
positions[hook] = row if hook
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
hooks.each do |full, list|
|
45
|
+
|
46
|
+
offset = nil
|
47
|
+
name = nil
|
48
|
+
|
49
|
+
if full =~ /^after_/
|
50
|
+
name = full[6..-1]
|
51
|
+
offset = 1
|
52
|
+
end
|
53
|
+
|
54
|
+
if full =~ /^before_/
|
55
|
+
name = full[7..-1]
|
56
|
+
offset = 0
|
57
|
+
end
|
58
|
+
|
59
|
+
index = run.index(positions[name])
|
60
|
+
|
61
|
+
if index && index >= 0
|
62
|
+
run.insert(index + offset, *list)
|
63
|
+
else
|
64
|
+
Pups.log.info "Skipped missing #{full} hook"
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def run
|
71
|
+
run_commands
|
72
|
+
rescue => e
|
73
|
+
puts
|
74
|
+
puts
|
75
|
+
puts "FAILED"
|
76
|
+
puts "-" * 20
|
77
|
+
puts "#{e.class}: #{e}"
|
78
|
+
puts "Location of failure: #{e.backtrace[0]}"
|
79
|
+
if @last_command
|
80
|
+
puts "#{@last_command[:command]} failed with the params #{@last_command[:params].inspect}"
|
81
|
+
end
|
82
|
+
exit 1
|
83
|
+
end
|
84
|
+
|
85
|
+
def run_commands
|
86
|
+
@config["run"].each do |item|
|
87
|
+
item.each do |k,v|
|
88
|
+
type = case k
|
89
|
+
when "exec" then Pups::ExecCommand
|
90
|
+
when "merge" then Pups::MergeCommand
|
91
|
+
when "replace" then Pups::ReplaceCommand
|
92
|
+
when "file" then Pups::FileCommand
|
93
|
+
else raise SyntaxError.new("Invalid run command #{k}")
|
94
|
+
end
|
95
|
+
|
96
|
+
@last_command = { command: k, params: v }
|
97
|
+
type.run(v, @params)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def interpolate_params(cmd)
|
103
|
+
self.class.interpolate_params(cmd,@params)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.interpolate_params(cmd, params)
|
107
|
+
return unless cmd
|
108
|
+
processed = cmd.dup
|
109
|
+
params.each do |k,v|
|
110
|
+
processed.gsub!("$#{k}", v.to_s)
|
111
|
+
end
|
112
|
+
processed
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
class Pups::ExecCommand < Pups::Command
|
4
|
+
attr_reader :commands, :cd
|
5
|
+
attr_accessor :background, :raise_on_fail, :stdin, :stop_signal
|
6
|
+
|
7
|
+
def self.terminate_async(opts={})
|
8
|
+
|
9
|
+
return unless defined? @@asyncs
|
10
|
+
|
11
|
+
Pups.log.info("Terminating async processes")
|
12
|
+
|
13
|
+
@@asyncs.each do |async|
|
14
|
+
Pups.log.info("Sending #{async[:stop_signal]} to #{async[:command]} pid: #{async[:pid]}")
|
15
|
+
Process.kill(async[:stop_signal],async[:pid]) rescue nil
|
16
|
+
end
|
17
|
+
|
18
|
+
@@asyncs.map do |async|
|
19
|
+
Thread.new do
|
20
|
+
begin
|
21
|
+
Timeout.timeout(opts[:wait] || 10) do
|
22
|
+
Process.wait(async[:pid]) rescue nil
|
23
|
+
end
|
24
|
+
rescue Timeout::Error
|
25
|
+
Pups.log.info("#{async[:command]} pid:#{async[:pid]} did not terminate cleanly, forcing termination!")
|
26
|
+
begin
|
27
|
+
Process.kill("KILL",async[:pid])
|
28
|
+
Process.wait(async[:pid])
|
29
|
+
rescue Errno::ESRCH
|
30
|
+
rescue Errno::ECHILD
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end.each(&:join)
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.from_hash(hash, params)
|
40
|
+
cmd = new(params, hash["cd"])
|
41
|
+
|
42
|
+
case c = hash["cmd"]
|
43
|
+
when String then cmd.add(c)
|
44
|
+
when Array then c.each{|i| cmd.add(i)}
|
45
|
+
end
|
46
|
+
|
47
|
+
cmd.background = hash["background"]
|
48
|
+
cmd.stop_signal = hash["stop_signal"] || "TERM"
|
49
|
+
cmd.raise_on_fail = hash["raise_on_fail"] if hash.key? "raise_on_fail"
|
50
|
+
cmd.stdin = interpolate_params(hash["stdin"], params)
|
51
|
+
|
52
|
+
cmd
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.from_str(str, params)
|
56
|
+
cmd = new(params)
|
57
|
+
cmd.add(str)
|
58
|
+
cmd
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(params, cd = nil)
|
62
|
+
@commands = []
|
63
|
+
@params = params
|
64
|
+
@cd = interpolate_params(cd)
|
65
|
+
@raise_on_fail = true
|
66
|
+
end
|
67
|
+
|
68
|
+
def add(cmd)
|
69
|
+
@commands << process_params(cmd)
|
70
|
+
end
|
71
|
+
|
72
|
+
def run
|
73
|
+
commands.each do |command|
|
74
|
+
Pups.log.info("> #{command}")
|
75
|
+
pid = spawn(command)
|
76
|
+
Pups.log.info(@result.readlines.join("\n")) if @result
|
77
|
+
pid
|
78
|
+
end
|
79
|
+
rescue
|
80
|
+
raise if @raise_on_fail
|
81
|
+
end
|
82
|
+
|
83
|
+
def spawn(command)
|
84
|
+
if background
|
85
|
+
pid = Process.spawn(command)
|
86
|
+
(@@asyncs ||= []) << {pid: pid, command: command, stop_signal: (stop_signal || "TERM")}
|
87
|
+
Thread.new do
|
88
|
+
Process.wait(pid)
|
89
|
+
@@asyncs.delete_if{|async| async[:pid] == pid}
|
90
|
+
end
|
91
|
+
return pid
|
92
|
+
end
|
93
|
+
|
94
|
+
IO.popen(command, "w+") do |f|
|
95
|
+
if stdin
|
96
|
+
# need a way to get stdout without blocking
|
97
|
+
Pups.log.info(stdin)
|
98
|
+
f.write stdin
|
99
|
+
f.close
|
100
|
+
else
|
101
|
+
Pups.log.info(f.readlines.join)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
raise RuntimeError.new("#{command} failed with return #{$?.inspect}") unless $? == 0
|
106
|
+
|
107
|
+
nil
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
def process_params(cmd)
|
112
|
+
processed = interpolate_params(cmd)
|
113
|
+
@cd ? "cd #{cd} && #{processed}" : processed
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Pups::FileCommand < Pups::Command
|
2
|
+
attr_accessor :path, :contents, :params, :type, :chmod
|
3
|
+
|
4
|
+
def self.from_hash(hash, params)
|
5
|
+
command = new
|
6
|
+
command.path = hash["path"]
|
7
|
+
command.contents = hash["contents"]
|
8
|
+
command.chmod = hash["chmod"]
|
9
|
+
command.params = params
|
10
|
+
|
11
|
+
command
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@params = {}
|
16
|
+
@type = :bash
|
17
|
+
end
|
18
|
+
|
19
|
+
def params=(p)
|
20
|
+
@params = p
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
path = interpolate_params(@path)
|
25
|
+
|
26
|
+
`mkdir -p #{File.dirname(path)}`
|
27
|
+
File.open(path, "w") do |f|
|
28
|
+
f.write(interpolate_params(contents))
|
29
|
+
end
|
30
|
+
if @chmod
|
31
|
+
`chmod #{@chmod} #{path}`
|
32
|
+
end
|
33
|
+
Pups.log.info("File > #{path} chmod: #{@chmod}")
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Pups::MergeCommand < Pups::Command
|
2
|
+
attr_reader :filename
|
3
|
+
attr_reader :merge_hash
|
4
|
+
|
5
|
+
def self.from_str(command, params)
|
6
|
+
new(command,params)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.parse_command(command)
|
10
|
+
split = command.split(" ")
|
11
|
+
raise ArgumentError.new("Invalid merge command #{command}") unless split[-1][0] == "$"
|
12
|
+
|
13
|
+
[split[0..-2].join(" ") , split[-1][1..-1]]
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(command, params)
|
17
|
+
@params = params
|
18
|
+
|
19
|
+
filename, target_param = Pups::MergeCommand.parse_command(command)
|
20
|
+
@filename = interpolate_params(filename)
|
21
|
+
@merge_hash = params[target_param]
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
merged = self.class.deep_merge(YAML.load_file(@filename), @merge_hash)
|
26
|
+
File.open(@filename,"w"){|f| f.write(merged.to_yaml) }
|
27
|
+
Pups.log.info("Merge: #{@filename} with: \n#{@merge_hash.inspect}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.deep_merge(first,second, *args)
|
31
|
+
args ||= []
|
32
|
+
merge_arrays = args.include? :merge_arrays
|
33
|
+
|
34
|
+
merger = proc { |key, v1, v2|
|
35
|
+
if Hash === v1 && Hash === v2
|
36
|
+
v1.merge(v2, &merger)
|
37
|
+
elsif Array === v1 && Array === v2
|
38
|
+
merge_arrays ? v1 + v2 : v2
|
39
|
+
elsif NilClass === v2
|
40
|
+
v1
|
41
|
+
else
|
42
|
+
v2
|
43
|
+
end
|
44
|
+
}
|
45
|
+
first.merge(second, &merger)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Pups::ReplaceCommand < Pups::Command
|
2
|
+
attr_accessor :text, :from, :to, :filename, :direction, :global
|
3
|
+
|
4
|
+
def self.from_hash(hash, params)
|
5
|
+
replacer = new(params)
|
6
|
+
replacer.from = guess_replace_type(hash["from"])
|
7
|
+
replacer.to = guess_replace_type(hash["to"])
|
8
|
+
replacer.text = File.read(hash["filename"])
|
9
|
+
replacer.filename = hash["filename"]
|
10
|
+
replacer.direction = hash["direction"].to_sym if hash["direction"]
|
11
|
+
replacer.global = hash["global"].to_s == "true"
|
12
|
+
replacer
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.guess_replace_type(item)
|
16
|
+
# evaling to get all the regex flags easily
|
17
|
+
item[0] == "/" ? eval(item) : item
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(params)
|
21
|
+
@params = params
|
22
|
+
end
|
23
|
+
|
24
|
+
def replaced_text
|
25
|
+
new_to = to
|
26
|
+
if String === to
|
27
|
+
new_to = interpolate_params(to)
|
28
|
+
end
|
29
|
+
if global
|
30
|
+
text.gsub(from,new_to)
|
31
|
+
elsif direction == :reverse
|
32
|
+
index = text.rindex(from)
|
33
|
+
text[0..index-1] << text[index..-1].sub(from,new_to)
|
34
|
+
else
|
35
|
+
text.sub(from,new_to)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def run
|
40
|
+
Pups.log.info("Replacing #{from.to_s} with #{to.to_s} in #{filename}")
|
41
|
+
File.open(filename, "w"){|f| f.write replaced_text }
|
42
|
+
end
|
43
|
+
end
|
data/lib/pups/runit.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
class Pups::Runit
|
2
|
+
|
3
|
+
attr_accessor :env, :exec, :cd, :name
|
4
|
+
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
def setup
|
11
|
+
`mkdir -p /etc/service/#{name}`
|
12
|
+
run = "/etc/service/#{name}/run"
|
13
|
+
File.open(run, "w") do |f|
|
14
|
+
f.write(run_script)
|
15
|
+
end
|
16
|
+
`chmod +x #{run}`
|
17
|
+
end
|
18
|
+
|
19
|
+
def run_script
|
20
|
+
"#!/bin/bash
|
21
|
+
exec 2>&1
|
22
|
+
#{env_script}
|
23
|
+
#{cd_script}
|
24
|
+
#{exec}
|
25
|
+
"
|
26
|
+
end
|
27
|
+
|
28
|
+
def cd_script
|
29
|
+
"cd #{@cd}" if @cd
|
30
|
+
end
|
31
|
+
|
32
|
+
def env_script
|
33
|
+
if @env
|
34
|
+
@env.map do |k,v|
|
35
|
+
"export #{k}=#{v}"
|
36
|
+
end.join("\n")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/lib/pups/version.rb
ADDED
data/pups.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pups/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pups"
|
8
|
+
spec.version = Pups::VERSION
|
9
|
+
spec.authors = ["Sam Saffron"]
|
10
|
+
spec.email = ["sam.saffron@gmail.com"]
|
11
|
+
spec.description = %q{Process orchestrator}
|
12
|
+
spec.summary = %q{Process orchestrator}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "minitest"
|
24
|
+
spec.add_development_dependency "guard"
|
25
|
+
spec.add_development_dependency "guard-minitest"
|
26
|
+
end
|
data/test/config_test.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class ConfigTest < MiniTest::Test
|
6
|
+
|
7
|
+
def test_config_from_env
|
8
|
+
ENV["HELLO"] = "world"
|
9
|
+
config = Config.new({})
|
10
|
+
assert_equal("world", config.params["$ENV_HELLO"])
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_integration
|
14
|
+
|
15
|
+
f = Tempfile.new("test")
|
16
|
+
f.close
|
17
|
+
|
18
|
+
config = <<YAML
|
19
|
+
params:
|
20
|
+
run: #{f.path}
|
21
|
+
run:
|
22
|
+
- exec: echo hello world >> #{f.path}
|
23
|
+
YAML
|
24
|
+
|
25
|
+
Config.new(YAML.load(config)).run
|
26
|
+
assert_equal("hello world", File.read(f.path).strip)
|
27
|
+
|
28
|
+
ensure
|
29
|
+
f.unlink
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_hooks
|
33
|
+
yaml = <<YAML
|
34
|
+
run:
|
35
|
+
- exec: 1
|
36
|
+
- exec:
|
37
|
+
hook: middle
|
38
|
+
cmd: 2
|
39
|
+
- exec: 3
|
40
|
+
hooks:
|
41
|
+
after_middle:
|
42
|
+
- exec: 2.1
|
43
|
+
before_middle:
|
44
|
+
- exec: 1.9
|
45
|
+
YAML
|
46
|
+
|
47
|
+
config = Config.load_config(yaml).config
|
48
|
+
assert_equal({ "exec" => 1.9 }, config["run"][1])
|
49
|
+
assert_equal({ "exec" => 2.1 }, config["run"][3])
|
50
|
+
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class ExecCommandTest < MiniTest::Test
|
6
|
+
|
7
|
+
def from_str(str, params={})
|
8
|
+
ExecCommand.from_str(str, params).commands
|
9
|
+
end
|
10
|
+
|
11
|
+
def from_hash(hash, params={})
|
12
|
+
ExecCommand.from_hash(hash, params).commands
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_simple_str_command
|
16
|
+
assert_equal(["do_something"],
|
17
|
+
from_str("do_something"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_simple_str_command_with_param
|
21
|
+
assert_equal(["hello world"],
|
22
|
+
from_str("hello $bob", {"bob" => "world"}))
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_nested_command
|
26
|
+
assert_equal(["first"],
|
27
|
+
from_hash("cmd" => "first"))
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_multi_commands
|
31
|
+
assert_equal(["first","second"],
|
32
|
+
from_hash("cmd" => ["first","second"]))
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_multi_commands_with_home
|
36
|
+
assert_equal(["cd /home/sam && first",
|
37
|
+
"cd /home/sam && second"],
|
38
|
+
from_hash("cmd" => ["first","second"],
|
39
|
+
"cd" => "/home/sam"))
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_exec_works
|
43
|
+
ExecCommand.from_str("ls",{}).run
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_fails_for_bad_command
|
47
|
+
assert_raises(Errno::ENOENT) do
|
48
|
+
ExecCommand.from_str("boom",{}).run
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_backgroud_task_do_not_fail
|
53
|
+
cmd = ExecCommand.new({})
|
54
|
+
cmd.background = true
|
55
|
+
cmd.add("sleep 10 && exit 1")
|
56
|
+
cmd.run
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_raise_on_fail
|
60
|
+
cmd = ExecCommand.new({})
|
61
|
+
cmd.add("chgrp -a")
|
62
|
+
cmd.raise_on_fail = false
|
63
|
+
cmd.run
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_stdin
|
67
|
+
|
68
|
+
`touch test_file`
|
69
|
+
cmd = ExecCommand.new({})
|
70
|
+
cmd.add("read test ; echo $test > test_file")
|
71
|
+
cmd.stdin = "hello"
|
72
|
+
cmd.run
|
73
|
+
|
74
|
+
assert_equal("hello\n", File.read("test_file"))
|
75
|
+
|
76
|
+
ensure
|
77
|
+
File.delete("test_file")
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_fails_for_non_zero_exit
|
81
|
+
assert_raises(RuntimeError) do
|
82
|
+
ExecCommand.from_str("chgrp -a",{}).run
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def test_can_terminate_async
|
88
|
+
cmd = ExecCommand.new({})
|
89
|
+
cmd.background = true
|
90
|
+
pid = cmd.spawn("sleep 10 && exit 1")
|
91
|
+
ExecCommand.terminate_async
|
92
|
+
assert_raises(Errno::ECHILD) do
|
93
|
+
Process.waitpid(pid,Process::WNOHANG)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_can_terminate_rogues
|
98
|
+
cmd = ExecCommand.new({})
|
99
|
+
cmd.background = true
|
100
|
+
pid = cmd.spawn("trap \"echo TERM && sleep 100\" TERM ; sleep 100")
|
101
|
+
# we need to give bash enough time to trap
|
102
|
+
sleep 0.01
|
103
|
+
|
104
|
+
ExecCommand.terminate_async(wait: 0.1)
|
105
|
+
|
106
|
+
assert_raises(Errno::ECHILD) do
|
107
|
+
Process.waitpid(pid,Process::WNOHANG)
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class FileCommandTest < MiniTest::Test
|
6
|
+
|
7
|
+
def test_simple_file_creation
|
8
|
+
tmp = Tempfile.new("test")
|
9
|
+
tmp.write("x")
|
10
|
+
tmp.close
|
11
|
+
|
12
|
+
|
13
|
+
cmd = FileCommand.new
|
14
|
+
cmd.path = tmp.path
|
15
|
+
cmd.contents = "hello $world"
|
16
|
+
cmd.params = {"world" => "world"}
|
17
|
+
cmd.run
|
18
|
+
|
19
|
+
assert_equal("hello world",
|
20
|
+
File.read(tmp.path))
|
21
|
+
ensure
|
22
|
+
tmp.close
|
23
|
+
tmp.unlink
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class MergeCommandTest < MiniTest::Test
|
6
|
+
def test_deep_merge_arrays
|
7
|
+
a = {a: {a: ["hi",1]}}
|
8
|
+
b = {a: {a: ["hi",2]}}
|
9
|
+
c = {a: {}}
|
10
|
+
|
11
|
+
d = Pups::MergeCommand.deep_merge(a,b,:merge_arrays)
|
12
|
+
d = Pups::MergeCommand.deep_merge(d,c,:merge_arrays)
|
13
|
+
|
14
|
+
assert_equal(["hi", 1,"hi", 2], d[:a][:a])
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_merges
|
18
|
+
|
19
|
+
source = <<YAML
|
20
|
+
user:
|
21
|
+
name: "bob"
|
22
|
+
password: "xyz"
|
23
|
+
YAML
|
24
|
+
|
25
|
+
f = Tempfile.new("test")
|
26
|
+
f.write source
|
27
|
+
f.close
|
28
|
+
|
29
|
+
merge = <<YAML
|
30
|
+
user:
|
31
|
+
name: "bob2"
|
32
|
+
YAML
|
33
|
+
|
34
|
+
MergeCommand.from_str("#{f.path} $yaml", {"yaml" => YAML.load(merge) }).run
|
35
|
+
|
36
|
+
changed = YAML.load_file(f.path)
|
37
|
+
|
38
|
+
assert_equal({"user" => {
|
39
|
+
"name" => "bob2",
|
40
|
+
"password" => "xyz"
|
41
|
+
}}, changed)
|
42
|
+
|
43
|
+
def test_deep_merge_nil
|
44
|
+
a = {param: {venison: "yes please"}}
|
45
|
+
b = {param: nil}
|
46
|
+
|
47
|
+
r1 = Pups::MergeCommand.deep_merge(a,b)
|
48
|
+
r2 = Pups::MergeCommand.deep_merge(b,a)
|
49
|
+
|
50
|
+
assert_equal({venison: "yes please"}, r1[:param])
|
51
|
+
assert_equal({venison: "yes please"}, r2[:param])
|
52
|
+
end
|
53
|
+
|
54
|
+
ensure
|
55
|
+
f.unlink
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module Pups
|
5
|
+
class ReplaceCommandTest < MiniTest::Test
|
6
|
+
|
7
|
+
def test_simple
|
8
|
+
command = ReplaceCommand.new({})
|
9
|
+
command.text = "hello world"
|
10
|
+
command.from = /he[^o]+o/
|
11
|
+
command.to = "world"
|
12
|
+
|
13
|
+
assert_equal("world world", command.replaced_text)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_reverse
|
17
|
+
source = <<SCR
|
18
|
+
1 one thousand 1
|
19
|
+
1 one thousand 1
|
20
|
+
1 one thousand 1
|
21
|
+
SCR
|
22
|
+
|
23
|
+
f = Tempfile.new("test")
|
24
|
+
f.write source
|
25
|
+
f.close
|
26
|
+
|
27
|
+
hash = {
|
28
|
+
"filename" => f.path,
|
29
|
+
"from" => "/one t.*d/",
|
30
|
+
"to" => "hello world",
|
31
|
+
"direction" => "reverse"
|
32
|
+
}
|
33
|
+
|
34
|
+
command = ReplaceCommand.from_hash(hash, {})
|
35
|
+
|
36
|
+
assert_equal("1 one thousand 1\n1 one thousand 1\n1 hello world 1\n", command.replaced_text)
|
37
|
+
ensure
|
38
|
+
f.unlink
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_global
|
42
|
+
source = <<SCR
|
43
|
+
one
|
44
|
+
one
|
45
|
+
one
|
46
|
+
SCR
|
47
|
+
|
48
|
+
f = Tempfile.new("test")
|
49
|
+
f.write source
|
50
|
+
f.close
|
51
|
+
|
52
|
+
hash = {
|
53
|
+
"filename" => f.path,
|
54
|
+
"from" => "/one/",
|
55
|
+
"to" => "two",
|
56
|
+
"global" => "true"
|
57
|
+
}
|
58
|
+
|
59
|
+
command = ReplaceCommand.from_hash(hash, {})
|
60
|
+
|
61
|
+
assert_equal("two\ntwo\ntwo\n", command.replaced_text)
|
62
|
+
ensure
|
63
|
+
f.unlink
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_replace_with_env
|
68
|
+
source = "123"
|
69
|
+
|
70
|
+
f = Tempfile.new("test")
|
71
|
+
f.write source
|
72
|
+
f.close
|
73
|
+
|
74
|
+
hash = {
|
75
|
+
"filename" => f.path,
|
76
|
+
"from" => "123",
|
77
|
+
"to" => "hello $hellos"
|
78
|
+
}
|
79
|
+
|
80
|
+
command = ReplaceCommand.from_hash(hash, {"hello" => "world"})
|
81
|
+
assert_equal("hello worlds", command.replaced_text)
|
82
|
+
|
83
|
+
ensure
|
84
|
+
f.unlink
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_parse
|
88
|
+
|
89
|
+
source = <<SCR
|
90
|
+
this {
|
91
|
+
is a test
|
92
|
+
}
|
93
|
+
SCR
|
94
|
+
|
95
|
+
f = Tempfile.new("test")
|
96
|
+
f.write source
|
97
|
+
f.close
|
98
|
+
|
99
|
+
hash = {
|
100
|
+
"filename" => f.path,
|
101
|
+
"from" => "/this[^\}]+\}/m",
|
102
|
+
"to" => "hello world"
|
103
|
+
}
|
104
|
+
|
105
|
+
command = ReplaceCommand.from_hash(hash, {})
|
106
|
+
|
107
|
+
assert_equal("hello world", command.replaced_text.strip)
|
108
|
+
ensure
|
109
|
+
f.unlink
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pups
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sam Saffron
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard-minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Process orchestrator
|
84
|
+
email:
|
85
|
+
- sam.saffron@gmail.com
|
86
|
+
executables:
|
87
|
+
- pups
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- Gemfile
|
93
|
+
- Guardfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/pups
|
98
|
+
- lib/pups.rb
|
99
|
+
- lib/pups/cli.rb
|
100
|
+
- lib/pups/command.rb
|
101
|
+
- lib/pups/config.rb
|
102
|
+
- lib/pups/exec_command.rb
|
103
|
+
- lib/pups/file_command.rb
|
104
|
+
- lib/pups/merge_command.rb
|
105
|
+
- lib/pups/replace_command.rb
|
106
|
+
- lib/pups/runit.rb
|
107
|
+
- lib/pups/version.rb
|
108
|
+
- pups.gemspec
|
109
|
+
- test/config_test.rb
|
110
|
+
- test/exec_command_test.rb
|
111
|
+
- test/file_command_test.rb
|
112
|
+
- test/merge_command_test.rb
|
113
|
+
- test/replace_command_test.rb
|
114
|
+
- test/test_helper.rb
|
115
|
+
homepage: ''
|
116
|
+
licenses:
|
117
|
+
- MIT
|
118
|
+
metadata: {}
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubyforge_project:
|
135
|
+
rubygems_version: 2.2.2
|
136
|
+
signing_key:
|
137
|
+
specification_version: 4
|
138
|
+
summary: Process orchestrator
|
139
|
+
test_files:
|
140
|
+
- test/config_test.rb
|
141
|
+
- test/exec_command_test.rb
|
142
|
+
- test/file_command_test.rb
|
143
|
+
- test/merge_command_test.rb
|
144
|
+
- test/replace_command_test.rb
|
145
|
+
- test/test_helper.rb
|