floe 0.12.0 → 0.13.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/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
|