floe 0.12.0 → 0.13.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -1
- data/lib/floe/container_runner/docker.rb +1 -1
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/choice_rule/data.rb +46 -9
- data/lib/floe/workflow/choice_rule.rb +31 -6
- data/lib/floe/workflow/intrinsic_function/parser.rb +9 -1
- data/lib/floe/workflow/intrinsic_function/transformer.rb +76 -10
- data/lib/floe/workflow/path.rb +12 -1
- data/lib/floe/workflow/state.rb +21 -3
- data/lib/floe.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6fb1e855fda7d4a00a72e494b820a89304350945c14a71675d5cc6306e69f079
|
4
|
+
data.tar.gz: 9780e210073c41b4a5c534bdabd301c2df6275c13328dbb1ee43569470e8d590
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bec5c7f6337c74258e185b76abfc3953f05ef16cf4de640972d23b7dfa81bf4439df3ce00dd539c618f7d18905d783bfa55dc777365e1a48969942b182448107
|
7
|
+
data.tar.gz: cf584d349c69927dec6945fe21d652b79e96f6228754b1a91a57b3241825661ed6d977a7d53e3ebfae526551da2a4a573148e9d855840d8edc52b8ebeffc948b
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,23 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
4
4
|
|
5
5
|
## [Unreleased]
|
6
6
|
|
7
|
+
## [0.13.1] - 2024-08-16
|
8
|
+
### Fixed
|
9
|
+
- Fix podman/docker container_ref trailing newline ([#265](https://github.com/ManageIQ/floe/pull/265))
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
- Improve type check for States.Hash ([#261](https://github.com/ManageIQ/floe/pull/261))
|
13
|
+
- Use Numeric over Integer || Float ([#264](https://github.com/ManageIQ/floe/pull/264))
|
14
|
+
- In ChoiceRule::Data, parse compare key and variable ([#257](https://github.com/ManageIQ/floe/pull/257))
|
15
|
+
|
16
|
+
## [0.13.0] - 2024-08-12
|
17
|
+
### Added
|
18
|
+
- Choice rule payload validation path ([#253](https://github.com/ManageIQ/floe/pull/253))
|
19
|
+
- Intrinsics JsonToString and StringToJson ([#256](https://github.com/ManageIQ/floe/pull/256))
|
20
|
+
- Add States.Format intrinsic function ([#258](https://github.com/ManageIQ/floe/pull/258))
|
21
|
+
- Intrinsics States.JsonMerge ([#255](https://github.com/ManageIQ/floe/pull/255))
|
22
|
+
- Enable support for Hashes in States.Hash ([#260](https://github.com/ManageIQ/floe/pull/260))
|
23
|
+
|
7
24
|
## [0.12.0] - 2024-07-31
|
8
25
|
### Added
|
9
26
|
- Set Floe.logger.level if DEBUG env var set ([#234](https://github.com/ManageIQ/floe/pull/234))
|
@@ -225,7 +242,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
225
242
|
### Added
|
226
243
|
- Initial release
|
227
244
|
|
228
|
-
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.
|
245
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.13.1...HEAD
|
246
|
+
[0.13.1]: https://github.com/ManageIQ/floe/compare/v0.13.0...v0.13.1
|
247
|
+
[0.13.0]: https://github.com/ManageIQ/floe/compare/v0.12.0...v0.13.0
|
229
248
|
[0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
|
230
249
|
[0.11.3]: https://github.com/ManageIQ/floe/compare/v0.11.2...v0.11.3
|
231
250
|
[0.11.2]: https://github.com/ManageIQ/floe/compare/v0.11.1...v0.11.2
|
@@ -129,7 +129,7 @@ module Floe
|
|
129
129
|
logger.debug("Running #{AwesomeSpawn.build_command_line(self.class::DOCKER_COMMAND, params)}")
|
130
130
|
|
131
131
|
result = docker!(*params)
|
132
|
-
result.output
|
132
|
+
result.output.chomp
|
133
133
|
end
|
134
134
|
|
135
135
|
def run_container_params(image, env, secrets_file)
|
data/lib/floe/version.rb
CHANGED
@@ -4,15 +4,26 @@ module Floe
|
|
4
4
|
class Workflow
|
5
5
|
class ChoiceRule
|
6
6
|
class Data < Floe::Workflow::ChoiceRule
|
7
|
+
COMPARE_KEYS = %w[IsNull IsPresent IsNumeric IsString IsBoolean IsTimestamp String Numeric Boolean Timestamp].freeze
|
8
|
+
|
9
|
+
attr_reader :variable, :compare_key, :value, :path
|
10
|
+
|
11
|
+
def initialize(_workflow, _name, payload)
|
12
|
+
super
|
13
|
+
|
14
|
+
@variable = parse_path("Variable", payload)
|
15
|
+
parse_compare_key
|
16
|
+
@value = path ? parse_path(compare_key, payload) : payload[compare_key]
|
17
|
+
end
|
18
|
+
|
7
19
|
def true?(context, input)
|
20
|
+
return presence_check(context, input) if compare_key == "IsPresent"
|
21
|
+
|
8
22
|
lhs = variable_value(context, input)
|
9
23
|
rhs = compare_value(context, input)
|
10
24
|
|
11
|
-
validate!(lhs)
|
12
|
-
|
13
25
|
case compare_key
|
14
26
|
when "IsNull" then is_null?(lhs)
|
15
|
-
when "IsPresent" then is_present?(lhs)
|
16
27
|
when "IsNumeric" then is_numeric?(lhs)
|
17
28
|
when "IsString" then is_string?(lhs)
|
18
29
|
when "IsBoolean" then is_boolean?(lhs)
|
@@ -47,8 +58,21 @@ module Floe
|
|
47
58
|
|
48
59
|
private
|
49
60
|
|
50
|
-
def
|
51
|
-
|
61
|
+
def presence_check(context, input)
|
62
|
+
# Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
|
63
|
+
# If true then return true when present.
|
64
|
+
# If false then return true when not present.
|
65
|
+
rhs = compare_value(context, input)
|
66
|
+
# Don't need the variable_value, just need to see if the path finds the value.
|
67
|
+
variable_value(context, input)
|
68
|
+
|
69
|
+
# The variable_value is present
|
70
|
+
# If rhs is true, then presence check was successful, return true.
|
71
|
+
rhs
|
72
|
+
rescue Floe::PathError
|
73
|
+
# variable_value is not present. (the path lookup threw an error)
|
74
|
+
# If rhs is false, then it successfully wasn't present, return true.
|
75
|
+
!rhs
|
52
76
|
end
|
53
77
|
|
54
78
|
def is_null?(value) # rubocop:disable Naming/PredicateName
|
@@ -60,7 +84,7 @@ module Floe
|
|
60
84
|
end
|
61
85
|
|
62
86
|
def is_numeric?(value) # rubocop:disable Naming/PredicateName
|
63
|
-
value.kind_of?(
|
87
|
+
value.kind_of?(Numeric)
|
64
88
|
end
|
65
89
|
|
66
90
|
def is_string?(value) # rubocop:disable Naming/PredicateName
|
@@ -80,12 +104,25 @@ module Floe
|
|
80
104
|
false
|
81
105
|
end
|
82
106
|
|
83
|
-
def
|
84
|
-
@compare_key
|
107
|
+
def parse_compare_key
|
108
|
+
@compare_key = payload.keys.detect { |key| key.match?(/^(#{COMPARE_KEYS.join("|")})/) }
|
109
|
+
parser_error!("requires a compare key") unless compare_key
|
110
|
+
|
111
|
+
@path = compare_key.end_with?("Path")
|
85
112
|
end
|
86
113
|
|
87
114
|
def compare_value(context, input)
|
88
|
-
|
115
|
+
path ? value.value(context, input) : value
|
116
|
+
end
|
117
|
+
|
118
|
+
def variable_value(context, input)
|
119
|
+
variable.value(context, input)
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_path(field_name, payload)
|
123
|
+
value = payload[field_name]
|
124
|
+
missing_field_error!(field_name) unless value
|
125
|
+
wrap_parser_error(field_name, value) { Path.new(value) }
|
89
126
|
end
|
90
127
|
end
|
91
128
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module Floe
|
4
4
|
class Workflow
|
5
5
|
class ChoiceRule
|
6
|
+
include ValidationMixin
|
7
|
+
|
6
8
|
class << self
|
7
9
|
def build(workflow, name, payload)
|
8
10
|
if (sub_payloads = payload["Not"])
|
@@ -25,15 +27,15 @@ module Floe
|
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
28
|
-
attr_reader :next, :payload, :
|
30
|
+
attr_reader :next, :payload, :children, :name
|
29
31
|
|
30
|
-
def initialize(
|
32
|
+
def initialize(workflow, name, payload, children = nil)
|
31
33
|
@name = name
|
32
34
|
@payload = payload
|
33
35
|
@children = children
|
36
|
+
@next = payload["Next"]
|
34
37
|
|
35
|
-
|
36
|
-
@variable = payload["Variable"]
|
38
|
+
validate_next!(workflow)
|
37
39
|
end
|
38
40
|
|
39
41
|
def true?(*)
|
@@ -42,8 +44,31 @@ module Floe
|
|
42
44
|
|
43
45
|
private
|
44
46
|
|
45
|
-
def
|
46
|
-
|
47
|
+
def validate_next!(workflow)
|
48
|
+
if is_child?
|
49
|
+
# non-top level nodes don't allow a next
|
50
|
+
invalid_field_error!("Next", @next, "not allowed in a child rule") if @next
|
51
|
+
elsif !@next
|
52
|
+
# top level nodes require a next
|
53
|
+
missing_field_error!("Next")
|
54
|
+
elsif !workflow_state?(@next, workflow)
|
55
|
+
# top level nodes require a next field that is found
|
56
|
+
invalid_field_error!("Next", @next, "is not found in \"States\"")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# returns true if this is a child rule underneath an And/Or/Not
|
61
|
+
# {
|
62
|
+
# "Or": [
|
63
|
+
# {"Variable": "$.foo", "IsString": true},
|
64
|
+
# {"Variable": "$.foo", "IsBoolean": true}
|
65
|
+
# ], "Next": "Finished"
|
66
|
+
# }
|
67
|
+
#
|
68
|
+
# The Or node, has no conjunction parent, so it is not a child (requires a Next)
|
69
|
+
# The 2 Data nodes have a conjunction parent, so each one is a child (do not allow a Next)
|
70
|
+
def is_child? # rubocop:disable Naming/PredicateName
|
71
|
+
!(%w[And Or Not] & name[0..-2]).empty?
|
47
72
|
end
|
48
73
|
end
|
49
74
|
end
|
@@ -54,6 +54,9 @@ module Floe
|
|
54
54
|
end
|
55
55
|
|
56
56
|
[
|
57
|
+
:states_format, "States.Format",
|
58
|
+
:states_string_to_json, "States.StringToJson",
|
59
|
+
:states_json_to_string, "States.JsonToString",
|
57
60
|
:states_array, "States.Array",
|
58
61
|
:states_array_partition, "States.ArrayPartition",
|
59
62
|
:states_array_contains, "States.ArrayContains",
|
@@ -64,6 +67,7 @@ module Floe
|
|
64
67
|
:states_base64_encode, "States.Base64Encode",
|
65
68
|
:states_base64_decode, "States.Base64Decode",
|
66
69
|
:states_hash, "States.Hash",
|
70
|
+
:states_json_merge, "States.JsonMerge",
|
67
71
|
:states_math_random, "States.MathRandom",
|
68
72
|
:states_math_add, "States.MathAdd",
|
69
73
|
:states_string_split, "States.StringSplit",
|
@@ -77,7 +81,10 @@ module Floe
|
|
77
81
|
end
|
78
82
|
|
79
83
|
rule(:expression) do
|
80
|
-
|
84
|
+
states_format |
|
85
|
+
states_string_to_json |
|
86
|
+
states_json_to_string |
|
87
|
+
states_array |
|
81
88
|
states_array_partition |
|
82
89
|
states_array_contains |
|
83
90
|
states_array_range |
|
@@ -87,6 +94,7 @@ module Floe
|
|
87
94
|
states_base64_encode |
|
88
95
|
states_base64_decode |
|
89
96
|
states_hash |
|
97
|
+
states_json_merge |
|
90
98
|
states_math_random |
|
91
99
|
states_math_add |
|
92
100
|
states_string_split |
|
@@ -9,6 +9,7 @@ module Floe
|
|
9
9
|
class IntrinsicFunction
|
10
10
|
class Transformer < Parslet::Transform
|
11
11
|
OptionalArg = Struct.new(:type)
|
12
|
+
VariadicArgs = Struct.new(:type)
|
12
13
|
|
13
14
|
class << self
|
14
15
|
def process_args(args, function, signature = nil)
|
@@ -37,20 +38,33 @@ module Floe
|
|
37
38
|
|
38
39
|
def check_arity(args, function, signature)
|
39
40
|
if signature.any?(OptionalArg)
|
40
|
-
|
41
|
-
signature_size = (
|
41
|
+
signature_required = signature.reject { |a| a.kind_of?(OptionalArg) }
|
42
|
+
signature_size = (signature_required.size..signature.size)
|
42
43
|
|
43
44
|
raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature_size})" unless signature_size.include?(args.size)
|
45
|
+
elsif signature.any?(VariadicArgs)
|
46
|
+
signature_required = signature.reject { |a| a.kind_of?(VariadicArgs) }
|
47
|
+
|
48
|
+
raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected at least #{signature_required.size})" unless args.size >= signature_required.size
|
44
49
|
else
|
45
50
|
raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature.size})" unless signature.size == args.size
|
46
51
|
end
|
47
52
|
end
|
48
53
|
|
49
54
|
def check_types(args, function, signature)
|
55
|
+
# Adjust the signature for VariadicArgs to create a copy of the expected type for each given arg
|
56
|
+
if signature.last.kind_of?(VariadicArgs)
|
57
|
+
signature = signature[0..-2] + Array.new(args.size - signature.size + 1, signature.last.type)
|
58
|
+
end
|
59
|
+
|
50
60
|
args.zip(signature).each_with_index do |(arg, type), index|
|
51
61
|
type = type.type if type.kind_of?(OptionalArg)
|
52
62
|
|
53
|
-
|
63
|
+
if type.kind_of?(Array)
|
64
|
+
raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected one of #{type.join(", ")})" unless type.any? { |t| arg.kind_of?(t) }
|
65
|
+
else
|
66
|
+
raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type)
|
67
|
+
end
|
54
68
|
end
|
55
69
|
end
|
56
70
|
end
|
@@ -63,6 +77,50 @@ module Floe
|
|
63
77
|
rule(:number => simple(:v)) { v.match(/[eE.]/) ? Float(v) : Integer(v) }
|
64
78
|
rule(:jsonpath => simple(:v)) { Floe::Workflow::Path.value(v.to_s, context, input) }
|
65
79
|
|
80
|
+
STATES_FORMAT_PLACEHOLDER = /(?<!\\)\{\}/.freeze
|
81
|
+
|
82
|
+
rule(:states_format => {:args => subtree(:args)}) do
|
83
|
+
args = Transformer.process_args(args(), "States.Format", [String, VariadicArgs[[String, TrueClass, FalseClass, Numeric, NilClass]]])
|
84
|
+
str, *rest = *args
|
85
|
+
|
86
|
+
# TODO: Handle templates with escaped characters, including invalid templates
|
87
|
+
# See https://states-language.net/#intrinsic-functions (number 6)
|
88
|
+
|
89
|
+
expected_args = str.scan(STATES_FORMAT_PLACEHOLDER).size
|
90
|
+
actual_args = rest.size
|
91
|
+
if expected_args != actual_args
|
92
|
+
raise ArgumentError, "number of arguments to States.Format do not match the occurrences of {} (given #{actual_args}, expected #{expected_args})"
|
93
|
+
end
|
94
|
+
|
95
|
+
rest.each do |arg|
|
96
|
+
str = str.sub(STATES_FORMAT_PLACEHOLDER, arg.nil? ? "null" : arg.to_s)
|
97
|
+
end
|
98
|
+
|
99
|
+
# TODO: Handle arguments that have escape characters within them but are interpolated
|
100
|
+
str.gsub!("\\'", "'")
|
101
|
+
str.gsub!("\\{", "{")
|
102
|
+
str.gsub!("\\}", "}")
|
103
|
+
str.gsub!("\\\\", "\\")
|
104
|
+
|
105
|
+
str
|
106
|
+
end
|
107
|
+
|
108
|
+
rule(:states_json_to_string => {:args => subtree(:args)}) do
|
109
|
+
args = Transformer.process_args(args(), "States.JsonToString", [Object])
|
110
|
+
json = args.first
|
111
|
+
|
112
|
+
JSON.generate(json)
|
113
|
+
end
|
114
|
+
|
115
|
+
rule(:states_string_to_json => {:args => subtree(:args)}) do
|
116
|
+
args = Transformer.process_args(args(), "States.StringToJson", [String])
|
117
|
+
str = args.first
|
118
|
+
|
119
|
+
JSON.parse(str)
|
120
|
+
rescue JSON::ParserError => e
|
121
|
+
raise ArgumentError, "invalid value for argument 1 to States.StringToJson (invalid json: #{e.message})"
|
122
|
+
end
|
123
|
+
|
66
124
|
rule(:states_array => {:args => subtree(:args)}) do
|
67
125
|
Transformer.process_args(args, "States.Array")
|
68
126
|
end
|
@@ -133,12 +191,8 @@ module Floe
|
|
133
191
|
end
|
134
192
|
|
135
193
|
rule(:states_hash => {:args => subtree(:args)}) do
|
136
|
-
args = Transformer.process_args(args(), "States.Hash", [
|
194
|
+
args = Transformer.process_args(args(), "States.Hash", [[String, TrueClass, FalseClass, Numeric, Array, Hash], String])
|
137
195
|
data, algorithm = *args
|
138
|
-
raise NotImplementedError if data.kind_of?(Hash)
|
139
|
-
if data.nil?
|
140
|
-
raise ArgumentError, "invalid value for argument 1 to States.Hash (given null, expected non-null)"
|
141
|
-
end
|
142
196
|
|
143
197
|
algorithms = %w[MD5 SHA-1 SHA-256 SHA-384 SHA-512]
|
144
198
|
unless algorithms.include?(algorithm)
|
@@ -147,8 +201,20 @@ module Floe
|
|
147
201
|
|
148
202
|
require "openssl"
|
149
203
|
algorithm = algorithm.sub("-", "")
|
150
|
-
data = data
|
151
|
-
OpenSSL::Digest.hexdigest(algorithm, data)
|
204
|
+
data = JSON.generate(data) unless data.kind_of?(String)
|
205
|
+
OpenSSL::Digest.hexdigest(algorithm, data).force_encoding("UTF-8")
|
206
|
+
end
|
207
|
+
|
208
|
+
rule(:states_json_merge => {:args => subtree(:args)}) do
|
209
|
+
args = Transformer.process_args(args(), "States.JsonMerge", [Hash, Hash, [TrueClass, FalseClass]])
|
210
|
+
left, right, deep = *args
|
211
|
+
|
212
|
+
if deep
|
213
|
+
# NOTE: not implemented by AWS Step Functions and nuances not defined in docs
|
214
|
+
left.merge(right) { |_key, l, r| l.kind_of?(Hash) && r.kind_of?(Hash) ? l.merge(r) : r }
|
215
|
+
else
|
216
|
+
left.merge(right)
|
217
|
+
end
|
152
218
|
end
|
153
219
|
|
154
220
|
rule(:states_math_random => {:args => subtree(:args)}) do
|
data/lib/floe/workflow/path.rb
CHANGED
@@ -34,7 +34,18 @@ module Floe
|
|
34
34
|
return obj if path == "$"
|
35
35
|
|
36
36
|
results = JsonPath.on(obj, path)
|
37
|
-
results.count
|
37
|
+
case results.count
|
38
|
+
when 0
|
39
|
+
raise Floe::PathError, "Path [#{payload}] references an invalid value"
|
40
|
+
when 1
|
41
|
+
results.first
|
42
|
+
else
|
43
|
+
results
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
payload
|
38
49
|
end
|
39
50
|
end
|
40
51
|
end
|
data/lib/floe/workflow/state.rb
CHANGED
@@ -48,17 +48,27 @@ module Floe
|
|
48
48
|
return Errno::EAGAIN unless ready?(context)
|
49
49
|
|
50
50
|
finish(context)
|
51
|
+
rescue Floe::Error => e
|
52
|
+
mark_error(context, e)
|
51
53
|
end
|
52
54
|
|
53
55
|
def start(context)
|
56
|
+
mark_started(context)
|
57
|
+
end
|
58
|
+
|
59
|
+
def finish(context)
|
60
|
+
mark_finished(context)
|
61
|
+
end
|
62
|
+
|
63
|
+
def mark_started(context)
|
54
64
|
context.state["EnteredTime"] = Time.now.utc.iso8601
|
55
65
|
|
56
66
|
logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
|
57
67
|
end
|
58
68
|
|
59
|
-
def
|
60
|
-
finished_time
|
61
|
-
entered_time
|
69
|
+
def mark_finished(context)
|
70
|
+
finished_time = Time.now.utc
|
71
|
+
entered_time = Time.parse(context.state["EnteredTime"])
|
62
72
|
|
63
73
|
context.state["FinishedTime"] ||= finished_time.iso8601
|
64
74
|
context.state["Duration"] = finished_time - entered_time
|
@@ -69,6 +79,14 @@ module Floe
|
|
69
79
|
0
|
70
80
|
end
|
71
81
|
|
82
|
+
def mark_error(context, exception)
|
83
|
+
# InputPath or OutputPath were bad.
|
84
|
+
context.next_state = nil
|
85
|
+
context.output = {"Error" => "States.Runtime", "Cause" => exception.message}
|
86
|
+
# Since finish threw an exception, super was never called. Calling that now.
|
87
|
+
mark_finished(context)
|
88
|
+
end
|
89
|
+
|
72
90
|
def ready?(context)
|
73
91
|
!context.state_started? || !running?(context)
|
74
92
|
end
|
data/lib/floe.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: floe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ManageIQ Developers
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-08-
|
11
|
+
date: 2024-08-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: awesome_spawn
|