kennel 1.121.1 → 1.123.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3eb87db53998806797dc41a7a73bacc01e1134655c7165c0498e30d7344e8fb
4
- data.tar.gz: 5ce8b48a088fdae20fc4b49211ca229f3f8e028f654eaa489b49ebf6de980646
3
+ metadata.gz: f09678f23d63da04d4b5db81eaef471bc0aa437b62ac75082f7bd161f8cd38e4
4
+ data.tar.gz: a570cc2617295581459a5977727049c243d492312554496f0b1beb432ad8efe8
5
5
  SHA512:
6
- metadata.gz: a22b79c15a1e7c29947011de54e58fbc5a3c71d988bf2a65e046304d6c890e0472405b548c06144b6f1f9f9fe2a6a91ed1a0608e59dcd925fe73b7023f9f91fd
7
- data.tar.gz: a3afc60d010f1364a46439d4f649d770cb1a76bb720856c9beada4cd89f4ab932b9aa1dfa8470feb97de969a0230752f94ec3a92f4e5a00275fa18fecdb46651
6
+ metadata.gz: 560a1daae6cb797b0e53e06a694e5624667b5597d3d6fb5c783e0166154a8f594064d7bfc1f64fb541723e2b633130d4e8039a8caa3434145552a63114a88678
7
+ data.tar.gz: '091c93ee61b87146c69e6f8782aab7a3cf2c349a03936f686a099eaca1a96d762193ee8385220fcc9d8c694edacf89aa57c6751e6e8a17bcffb37b1f913f9591'
data/Readme.md CHANGED
@@ -406,6 +406,14 @@ https://foo.datadog.com/monitor/123
406
406
  ### Find all monitors with No-Data
407
407
  `rake kennel:nodata TAG=team:foo`
408
408
 
409
+ ### Finding the tracking id of a resource
410
+
411
+ When trying to link resources together, this avoids having to go through datadog UI.
412
+
413
+ ```Bash
414
+ rake kennel:tracking_id ID=123 RESOURCE=monitor
415
+ ```
416
+
409
417
  <!-- NOT IN template/Readme.md -->
410
418
 
411
419
  ## Development
@@ -15,11 +15,13 @@ module Kennel
15
15
  end
16
16
 
17
17
  def open
18
- load_data
19
- expire_old_data
20
- yield self
21
- ensure
22
- persist
18
+ @data = load_data || {}
19
+ begin
20
+ expire_old_data
21
+ yield self
22
+ ensure
23
+ persist
24
+ end
23
25
  end
24
26
 
25
27
  def fetch(key, key_version)
@@ -35,12 +37,9 @@ module Kennel
35
37
  private
36
38
 
37
39
  def load_data
38
- @data =
39
- begin
40
- Marshal.load(File.read(@file)) # rubocop:disable Security/MarshalLoad
41
- rescue StandardError
42
- {}
43
- end
40
+ Marshal.load(File.read(@file)) # rubocop:disable Security/MarshalLoad
41
+ rescue Errno::ENOENT, TypeError, ArgumentError
42
+ nil
44
43
  end
45
44
 
46
45
  def persist
@@ -49,6 +48,7 @@ module Kennel
49
48
 
50
49
  Tempfile.create "kennel-file-cache", dir do |tmp|
51
50
  Marshal.dump @data, tmp
51
+ tmp.flush
52
52
  File.rename tmp.path, @file
53
53
  end
54
54
  end
@@ -24,7 +24,7 @@ module Kennel
24
24
  end
25
25
 
26
26
  def report(&block)
27
- output = Utils.strip_shell_control(Utils.tee_output(&block).strip)
27
+ output = Utils.tee_output(&block).strip
28
28
  rescue StandardError
29
29
  output = "Error:\n#{$ERROR_INFO.message}"
30
30
  raise
@@ -38,6 +38,8 @@ module Kennel
38
38
 
39
39
  case resource
40
40
  when "monitor"
41
+ raise "Import the synthetic test page and not the monitor" if data[:type] == "synthetics alert"
42
+
41
43
  # flatten monitor options so they are all on the base which is how Monitor builds them
42
44
  data.merge!(data.delete(:options))
43
45
  data.merge!(data.delete(:thresholds) || {})
@@ -60,6 +62,8 @@ module Kennel
60
62
  data[:critical] = data[:critical].to_i if data[:type] == "event alert"
61
63
 
62
64
  data[:type] = "query alert" if data[:type] == "metric alert"
65
+
66
+ link_composite_monitors(data)
63
67
  when "dashboard"
64
68
  widgets = data[:widgets]&.flat_map { |widget| widget.dig(:definition, :widgets) || [widget] }
65
69
  widgets&.each do |widget|
@@ -91,6 +95,18 @@ module Kennel
91
95
 
92
96
  private
93
97
 
98
+ def link_composite_monitors(data)
99
+ if data[:type] == "composite"
100
+ data[:query].gsub!(/\d+/) do |id|
101
+ object = Kennel.send(:api).show("monitor", id)
102
+ tracking_id = Kennel::Models::Monitor.parse_tracking_id(object)
103
+ tracking_id ? "%{#{tracking_id}}" : id
104
+ rescue StandardError # monitor not found
105
+ id # keep the id
106
+ end
107
+ end
108
+ end
109
+
94
110
  # reduce duplication in imports by using dry `q: :metadata` when possible
95
111
  def dry_up_widget_metadata!(widget)
96
112
  (widget.dig(:definition, :requests) || []).each do |request|
@@ -3,7 +3,6 @@ module Kennel
3
3
  module Models
4
4
  class Dashboard < Record
5
5
  include TemplateVariables
6
- include OptionalValidations
7
6
 
8
7
  READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
9
8
  :author_handle, :author_name, :modified_at, :deleted_at, :url, :is_read_only, :notify_list, :restricted_roles
@@ -87,8 +86,7 @@ module Kennel
87
86
  tags: -> do # not inherited by default to make onboarding to using dashboard tags simple
88
87
  team = project.team
89
88
  team.tag_dashboards ? team.tags : []
90
- end,
91
- id: -> { nil }
89
+ end
92
90
  )
93
91
 
94
92
  class << self
@@ -152,29 +150,24 @@ module Kennel
152
150
  end
153
151
  end
154
152
 
155
- def as_json
156
- return @json if @json
153
+ def build_json
157
154
  all_widgets = render_definitions(definitions) + widgets
158
155
  expand_q all_widgets
159
156
  tags = tags()
160
157
  tags_as_string = (tags.empty? ? "" : " (#{tags.join(" ")})")
161
158
 
162
- @json = {
159
+ json = super.merge(
163
160
  layout_type: layout_type,
164
161
  title: "#{title}#{tags_as_string}#{LOCK}",
165
162
  description: description,
166
163
  template_variables: render_template_variables,
167
164
  template_variable_presets: template_variable_presets,
168
165
  widgets: all_widgets
169
- }
170
-
171
- @json[:reflow_type] = reflow_type if reflow_type # setting nil breaks create with "ordered"
166
+ )
172
167
 
173
- @json[:id] = id if id
168
+ json[:reflow_type] = reflow_type if reflow_type # setting nil breaks create with "ordered"
174
169
 
175
- validate_json(@json) if validate
176
-
177
- @json
170
+ json
178
171
  end
179
172
 
180
173
  def self.url(id)
@@ -210,9 +203,8 @@ module Kennel
210
203
  end
211
204
 
212
205
  def validate_update!(_actuals, diffs)
213
- if bad_diff = diffs.find { |diff| diff[1] == "layout_type" }
214
- invalid! "Datadog does not allow update of #{bad_diff[1]} (#{bad_diff[2].inspect} -> #{bad_diff[3].inspect})"
215
- end
206
+ _, path, from, to = diffs.find { |diff| diff[1] == "layout_type" }
207
+ invalid_update!(path, from, to) if path
216
208
  end
217
209
 
218
210
  private
@@ -2,8 +2,6 @@
2
2
  module Kennel
3
3
  module Models
4
4
  class Monitor < Record
5
- include OptionalValidations
6
-
7
5
  RENOTIFY_INTERVALS = [0, 10, 20, 30, 40, 50, 60, 90, 120, 180, 240, 300, 360, 720, 1440].freeze # minutes
8
6
  OPTIONAL_SERVICE_CHECK_THRESHOLDS = [:ok, :warning].freeze
9
7
  READONLY_ATTRIBUTES = superclass::READONLY_ATTRIBUTES + [
@@ -41,7 +39,6 @@ module Kennel
41
39
  renotify_interval: -> { project.team.renotify_interval },
42
40
  warning: -> { nil },
43
41
  ok: -> { nil },
44
- id: -> { nil },
45
42
  notify_no_data: -> { true }, # datadog sets this to false by default, but true is the safer
46
43
  no_data_timeframe: -> { 60 },
47
44
  notify_audit: -> { MONITOR_OPTION_DEFAULTS.fetch(:notify_audit) },
@@ -56,9 +53,8 @@ module Kennel
56
53
  priority: -> { MONITOR_DEFAULTS.fetch(:priority) }
57
54
  )
58
55
 
59
- def as_json
60
- return @as_json if @as_json
61
- data = {
56
+ def build_json
57
+ data = super.merge(
62
58
  name: "#{name}#{LOCK}",
63
59
  type: type,
64
60
  query: query.strip,
@@ -79,9 +75,7 @@ module Kennel
79
75
  locked: false, # setting this to true prevents any edit and breaks updates when using replace workflow
80
76
  renotify_interval: renotify_interval || 0
81
77
  }
82
- }
83
-
84
- data[:id] = id if id
78
+ )
85
79
 
86
80
  options = data[:options]
87
81
  if data.fetch(:type) != "composite"
@@ -120,9 +114,7 @@ module Kennel
120
114
  options[:renotify_statuses] = statuses
121
115
  end
122
116
 
123
- validate_json(data) if validate
124
-
125
- @as_json = data
117
+ data
126
118
  end
127
119
 
128
120
  def resolve_linked_tracking_ids!(id_map, **args)
@@ -140,7 +132,7 @@ module Kennel
140
132
  # ensure type does not change, but not if it's metric->query which is supported and used by importer.rb
141
133
  _, path, from, to = diffs.detect { |_, path, _, _| path == "type" }
142
134
  if path && !(from == "metric alert" && to == "query alert")
143
- invalid! "Datadog does not allow update of #{path} (#{from.inspect} -> #{to.inspect})"
135
+ invalid_update!(path, from, to)
144
136
  end
145
137
  end
146
138
 
@@ -20,7 +20,7 @@ module Kennel
20
20
  def validated_parts
21
21
  all = parts
22
22
  unless all.is_a?(Array) && all.all? { |part| part.is_a?(Record) }
23
- invalid! "#parts must return an array of Records"
23
+ raise "Project #{kennel_id} #parts must return an array of Records"
24
24
  end
25
25
 
26
26
  validate_parts(all)
@@ -29,11 +29,6 @@ module Kennel
29
29
 
30
30
  private
31
31
 
32
- # let users know which project/resource failed when something happens during diffing where the backtrace is hidden
33
- def invalid!(message)
34
- raise ValidationError, "#{kennel_id} #{message}"
35
- end
36
-
37
32
  # hook for users to add custom validations via `prepend`
38
33
  def validate_parts(parts)
39
34
  end
@@ -2,6 +2,8 @@
2
2
  module Kennel
3
3
  module Models
4
4
  class Record < Base
5
+ include OptionalValidations
6
+
5
7
  # Apart from if you just don't like the default for some reason,
6
8
  # overriding MARKER_TEXT allows for namespacing within the same
7
9
  # Datadog account. If you run one Kennel setup with marker text
@@ -30,6 +32,8 @@ module Kennel
30
32
 
31
33
  settings :id, :kennel_id
32
34
 
35
+ defaults(id: nil)
36
+
33
37
  class << self
34
38
  def parse_any_url(url)
35
39
  subclasses.detect do |s|
@@ -96,7 +100,7 @@ module Kennel
96
100
  @tracking_id ||= begin
97
101
  id = "#{project.kennel_id}:#{kennel_id}"
98
102
  unless id.match?(ALLOWED_KENNEL_ID_REGEX) # <-> parse_tracking_id
99
- raise ValidationError, "#{id} must match #{ALLOWED_KENNEL_ID_REGEX}"
103
+ raise "#{id} must match #{ALLOWED_KENNEL_ID_REGEX}"
100
104
  end
101
105
  id
102
106
  end
@@ -108,7 +112,7 @@ module Kennel
108
112
  def add_tracking_id
109
113
  json = as_json
110
114
  if self.class.parse_tracking_id(json)
111
- invalid! "remove \"-- #{MARKER_TEXT}\" line it from #{self.class::TRACKING_FIELD} to copy a resource"
115
+ raise "#{tracking_id} Remove \"-- #{MARKER_TEXT}\" line from #{self.class::TRACKING_FIELD} to copy a resource"
112
116
  end
113
117
  json[self.class::TRACKING_FIELD] =
114
118
  "#{json[self.class::TRACKING_FIELD]}\n" \
@@ -119,9 +123,29 @@ module Kennel
119
123
  self.class.remove_tracking_id(as_json)
120
124
  end
121
125
 
126
+ def build_json
127
+ {
128
+ id: id
129
+ }.compact
130
+ end
131
+
132
+ def as_json
133
+ @as_json ||= begin
134
+ json = build_json
135
+ (id = json.delete(:id)) && json[:id] = id
136
+ validate_json(json) if validate
137
+ json
138
+ end
139
+ end
140
+
141
+ # Can raise DisallowedUpdateError
122
142
  def validate_update!(*)
123
143
  end
124
144
 
145
+ def invalid_update!(field, old_value, new_value)
146
+ raise DisallowedUpdateError, "#{tracking_id} Datadog does not allow update of #{field} (#{old_value.inspect} -> #{new_value.inspect})"
147
+ end
148
+
125
149
  private
126
150
 
127
151
  def resolve(value, type, id_map, force:)
@@ -133,20 +157,24 @@ module Kennel
133
157
  id.is_a?(String) && id.include?(":")
134
158
  end
135
159
 
136
- def resolve_link(tracking_id, type, id_map, force:)
137
- if id_map.new?(type.to_s, tracking_id)
160
+ def resolve_link(sought_tracking_id, sought_type, id_map, force:)
161
+ if id_map.new?(sought_type.to_s, sought_tracking_id)
138
162
  if force
139
- invalid!(
140
- "#{type} #{tracking_id} was referenced but is also created by the current run.\n" \
141
- "It could not be created because of a circular dependency, try creating only some of the resources"
142
- )
163
+ raise UnresolvableIdError, <<~MESSAGE
164
+ #{tracking_id} #{sought_type} #{sought_tracking_id} was referenced but is also created by the current run.
165
+ It could not be created because of a circular dependency. Try creating only some of the resources.
166
+ MESSAGE
143
167
  else
144
168
  nil # will be re-resolved after the linked object was created
145
169
  end
146
- elsif id = id_map.get(type.to_s, tracking_id)
170
+ elsif id = id_map.get(sought_type.to_s, sought_tracking_id)
147
171
  id
148
172
  else
149
- invalid! "Unable to find #{type} #{tracking_id} (does not exist and is not being created by the current run)"
173
+ raise UnresolvableIdError, <<~MESSAGE
174
+ #{tracking_id} Unable to find #{sought_type} #{sought_tracking_id}
175
+ This is either because it doesn't exist, and isn't being created by the current run;
176
+ or it does exist, but is being deleted.
177
+ MESSAGE
150
178
  end
151
179
  end
152
180
 
@@ -15,7 +15,6 @@ module Kennel
15
15
  settings :type, :description, :thresholds, :query, :tags, :monitor_ids, :monitor_tags, :name, :groups
16
16
 
17
17
  defaults(
18
- id: -> { nil },
19
18
  tags: -> { @project.tags },
20
19
  query: -> { DEFAULTS.fetch(:query) },
21
20
  description: -> { DEFAULTS.fetch(:description) },
@@ -24,35 +23,25 @@ module Kennel
24
23
  groups: -> { DEFAULTS.fetch(:groups) }
25
24
  )
26
25
 
27
- def initialize(*)
28
- super
29
- if thresholds.any? { |t| t[:warning] && t[:warning].to_f <= t[:critical].to_f }
30
- raise ValidationError, "Threshold warning must be greater-than critical value"
31
- end
32
- end
33
-
34
- def as_json
35
- return @as_json if @as_json
36
- data = {
26
+ def build_json
27
+ data = super.merge(
37
28
  name: "#{name}#{LOCK}",
38
29
  description: description,
39
30
  thresholds: thresholds,
40
31
  monitor_ids: monitor_ids,
41
32
  tags: tags.uniq,
42
33
  type: type
43
- }
34
+ )
44
35
 
45
36
  if v = query
46
37
  data[:query] = v
47
38
  end
48
- if v = id
49
- data[:id] = v
50
- end
39
+
51
40
  if v = groups
52
41
  data[:groups] = v
53
42
  end
54
43
 
55
- @as_json = data
44
+ data
56
45
  end
57
46
 
58
47
  def self.api_resource
@@ -89,6 +78,16 @@ module Kennel
89
78
 
90
79
  ignore_default(expected, actual, DEFAULTS)
91
80
  end
81
+
82
+ private
83
+
84
+ def validate_json(data)
85
+ super
86
+
87
+ if data[:thresholds].any? { |t| t[:warning] && t[:warning].to_f <= t[:critical].to_f }
88
+ invalid! "Threshold warning must be greater-than critical value"
89
+ end
90
+ end
92
91
  end
93
92
  end
94
93
  end
@@ -11,15 +11,14 @@ module Kennel
11
11
  settings :tags, :config, :message, :subtype, :type, :name, :locations, :options
12
12
 
13
13
  defaults(
14
- id: -> { nil },
15
14
  tags: -> { @project.tags },
16
15
  message: -> { "\n\n#{project.mention}" }
17
16
  )
18
17
 
19
- def as_json
20
- return @as_json if @as_json
18
+ def build_json
21
19
  locations = locations()
22
- data = {
20
+
21
+ super.merge(
23
22
  message: message,
24
23
  tags: tags,
25
24
  config: config,
@@ -28,13 +27,7 @@ module Kennel
28
27
  options: options,
29
28
  name: "#{name}#{LOCK}",
30
29
  locations: locations == :all ? LOCATIONS : locations
31
- }
32
-
33
- if v = id
34
- data[:id] = v
35
- end
36
-
37
- @as_json = data
30
+ )
38
31
  end
39
32
 
40
33
  def self.api_resource
@@ -4,7 +4,9 @@ require "benchmark"
4
4
  module Kennel
5
5
  class Progress
6
6
  # print what we are doing and a spinner until it is done ... then show how long it took
7
- def self.progress(name)
7
+ def self.progress(name, interval: 0.2, &block)
8
+ return progress_no_tty(name, &block) unless Kennel.err.tty?
9
+
8
10
  Kennel.err.print "#{name} ... "
9
11
 
10
12
  stop = false
@@ -16,15 +18,20 @@ module Kennel
16
18
  loop do
17
19
  break if stop
18
20
  Kennel.err.print animation[count % animation.size]
19
- sleep 0.2
21
+ sleep interval
20
22
  Kennel.err.print "\b"
21
23
  count += 1
22
24
  end
23
25
  end
24
26
 
25
- time = Benchmark.realtime { result = yield }
27
+ time = Benchmark.realtime { result = block.call }
26
28
 
27
29
  stop = true
30
+ begin
31
+ spinner.run # wake thread, so it stops itself
32
+ rescue ThreadError
33
+ # thread was already dead, but we can't check with .alive? since it's a race condition
34
+ end
28
35
  spinner.join
29
36
  Kennel.err.print "#{time.round(2)}s\n"
30
37
 
@@ -32,5 +39,17 @@ module Kennel
32
39
  ensure
33
40
  stop = true # make thread stop without killing it
34
41
  end
42
+
43
+ class << self
44
+ private
45
+
46
+ def progress_no_tty(name)
47
+ Kennel.err.puts "#{name} ..."
48
+ result = nil
49
+ time = Benchmark.realtime { result = yield }
50
+ Kennel.err.puts "#{name} ... #{time.round(2)}s"
51
+ result
52
+ end
53
+ end
35
54
  end
36
55
  end
data/lib/kennel/syncer.rb CHANGED
@@ -42,7 +42,7 @@ module Kennel
42
42
 
43
43
  def confirm
44
44
  return false if noop?
45
- return true if ENV["CI"] || !STDIN.tty?
45
+ return true if ENV["CI"] || !STDIN.tty? || !Kennel.err.tty?
46
46
  Utils.ask("Execute Plan ?")
47
47
  end
48
48
 
@@ -104,11 +104,11 @@ module Kennel
104
104
  def resolved?(e)
105
105
  assert_resolved e
106
106
  true
107
- rescue ValidationError
107
+ rescue UnresolvableIdError
108
108
  false
109
109
  end
110
110
 
111
- # raises ValidationError when not resolved
111
+ # raises UnresolvableIdError when not resolved
112
112
  def assert_resolved(e)
113
113
  resolve_linked_tracking_ids! [e], force: true
114
114
  end
data/lib/kennel/tasks.rb CHANGED
@@ -233,6 +233,17 @@ namespace :kennel do
233
233
  end
234
234
  end
235
235
 
236
+ desc "Resolve given id to kennel tracking-id RESOURCE= ID="
237
+ task tracking_id: "kennel:environment" do
238
+ resource = ENV.fetch("RESOURCE")
239
+ id = ENV.fetch("ID")
240
+ klass =
241
+ Kennel::Models::Record.subclasses.detect { |s| s.api_resource == resource } ||
242
+ raise("resource #{resource} not know")
243
+ object = Kennel.send(:api).show(resource, id)
244
+ Kennel.out.puts klass.parse_tracking_id(object)
245
+ end
246
+
236
247
  task :environment do
237
248
  Kennel::Tasks.load_environment
238
249
  end
data/lib/kennel/utils.rb CHANGED
@@ -42,7 +42,7 @@ module Kennel
42
42
  end
43
43
 
44
44
  def ask(question)
45
- Kennel.err.printf color(:red, "#{question} - press 'y' to continue: ")
45
+ Kennel.err.printf color(:red, "#{question} - press 'y' to continue: ", force: true)
46
46
  begin
47
47
  STDIN.gets.chomp == "y"
48
48
  rescue Interrupt # do not show a backtrace if user decides to Ctrl+C here
@@ -51,12 +51,10 @@ module Kennel
51
51
  end
52
52
  end
53
53
 
54
- def color(color, text)
55
- "\e[#{COLORS.fetch(color)}m#{text}\e[0m"
56
- end
54
+ def color(color, text, force: false)
55
+ return text unless force || Kennel.out.tty?
57
56
 
58
- def strip_shell_control(text)
59
- text.gsub(/\e\[\d+m(.*?)\e\[0m/, "\\1").gsub(/.#{Regexp.escape("\b")}/, "")
57
+ "\e[#{COLORS.fetch(color)}m#{text}\e[0m"
60
58
  end
61
59
 
62
60
  def capture_stdout
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Kennel
3
- VERSION = "1.121.1"
3
+ VERSION = "1.123.0"
4
4
  end
data/lib/kennel.rb CHANGED
@@ -40,8 +40,9 @@ module Teams
40
40
  end
41
41
 
42
42
  module Kennel
43
- class ValidationError < RuntimeError
44
- end
43
+ ValidationError = Class.new(RuntimeError)
44
+ UnresolvableIdError = Class.new(RuntimeError)
45
+ DisallowedUpdateError = Class.new(RuntimeError)
45
46
 
46
47
  include Kennel::Compatibility
47
48
 
data/template/Readme.md CHANGED
@@ -388,3 +388,11 @@ https://foo.datadog.com/monitor/123
388
388
  ### Find all monitors with No-Data
389
389
  `rake kennel:nodata TAG=team:foo`
390
390
 
391
+ ### Finding the tracking id of a resource
392
+
393
+ When trying to link resources together, this avoids having to go through datadog UI.
394
+
395
+ ```Bash
396
+ rake kennel:tracking_id ID=123 RESOURCE=monitor
397
+ ```
398
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kennel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.121.1
4
+ version: 1.123.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Grosser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-30 00:00:00.000000000 Z
11
+ date: 2022-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs