ecs_deploy 1.0.3 → 1.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4d2cb1f5439702efac5ec26e9cd0c47c6225e0106c05b6f73b7860098098488
4
- data.tar.gz: 29007dc6fdeb7b64b82a0113ac78ad00025ec9693bca1b238ccca3c507065c01
3
+ metadata.gz: a731ec39a6928bb4365fbb4784b91dd92737eb5b5796da19e3be8185d6bc10b8
4
+ data.tar.gz: '04758ae740d9ab1bb9108d5dcae1df72102fd161e0f96a38f1e30b461415bc7f'
5
5
  SHA512:
6
- metadata.gz: 42481f7631cebe62057fbd9e2c0fc6ba61ff0ee198e5d537958934df8a001c76ef5ddb6f8965c2f7db57d0098aac3b79cb43a677cdbaa95609107d29826892b8
7
- data.tar.gz: 28a2d8072e7f94a84301e0f0607fc02c469561d1f39da439cf87d3f7b64c12e301bb77a13bb96e0823870b43af36f5f35ee3e4984567ac57ebe5ae759342d61c
6
+ metadata.gz: 429dc1f441f7a67a973293adf8c48a62671ab3f5710cee596186a7fe5773b772b643b9ef879fb0b2143d7f7100754b6521ec6823094122fec85bfd189ca3c306
7
+ data.tar.gz: 03f568218e5e77e2c9b86560ac5dd4f41e19440f98da1ee102ee09057bc7ec8fdf7caa5366b56d06946d5887a0a60d8bae0c1ce04e80e0a50df7a51e40642da0
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ ruby-version: ['2.5', '2.6', '2.7', '3.0', '3.1', '3.2']
14
+
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+ - name: Set up Ruby
18
+ uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: ${{ matrix.ruby-version }}
21
+ bundler-cache: true
22
+ - name: Run tests
23
+ run: bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # v1.0
2
2
 
3
+ ## Release v1.0.4 - 2023/02/10
4
+
5
+ ### Bug fixes
6
+
7
+ - Fix Aws::AutoScaling::Errors::ValidationError https://github.com/reproio/ecs_deploy/pull/85
8
+
9
+ - Fix Timeout::Error that occurs in trigger_capacity_update https://github.com/reproio/ecs_deploy/pull/80
10
+
11
+ - use force a new deployment, when switching from launch type to capacity provider strategy on an existing service https://github.com/reproio/ecs_deploy/pull/75
12
+
13
+ ### Enhancement
14
+
15
+ - Run test with Ruby 3.2 https://github.com/reproio/ecs_deploy/pull/83
16
+
17
+ - Merge `propagate_tags` to service_options when updating service https://github.com/reproio/ecs_deploy/pull/82
18
+
19
+ - Show service event logs while waiting for services https://github.com/reproio/ecs_deploy/pull/81
20
+
21
+ - Stop supporting ruby 2.4 https://github.com/reproio/ecs_deploy/pull/79
22
+
23
+ - Display warning that desired count has reached max value https://github.com/reproio/ecs_deploy/pull/78
24
+
25
+ - Make draining feature opt-outable https://github.com/reproio/ecs_deploy/pull/77
26
+
27
+ - Add capacity_provider_strategy options to Service https://github.com/reproio/ecs_deploy/pull/74
28
+
3
29
  ## Release v1.0.3 - 2021/11/17
4
30
 
5
31
  ### Bug fixes
data/README.md CHANGED
@@ -215,6 +215,7 @@ auto_scaling_groups:
215
215
  # autoscaler will set the capacity to (buffer + desired_tasks * required_capacity).
216
216
  # Adjust this value if it takes much time to prepare ECS instances and launch new tasks.
217
217
  buffer: 1
218
+ disable_draining: false # cf. spot_instance_intrp_warns_queue_urls
218
219
  services:
219
220
  - name: repro-api-production
220
221
  step: 1
@@ -242,6 +243,7 @@ spot_fleet_requests:
242
243
  region: ap-northeast-1
243
244
  cluster: ecs-cluster-for-worker
244
245
  buffer: 1
246
+ disable_draining: false # cf. spot_instance_intrp_warns_queue_urls
245
247
  services:
246
248
  - name: repro-worker-production
247
249
  step: 1
@@ -261,11 +263,15 @@ spot_fleet_requests:
261
263
  state: ALARM
262
264
  prioritized_over_upscale_triggers: true
263
265
 
264
- # If you specify `spot_instance_intrp_warns_queue_urls` as SQS queue for spot instance interruption warnings,
265
- # autoscaler will polls them and set the state of instances to be intrrupted to "DRAINING".
266
- # autoscaler will also waits for the capacity of active instances in the cluster being decreased
267
- # when the capacity of spot fleet request is decreased,
268
- # so you should specify URLs or set the state of the instances to "DRAINING" manually.
266
+ # When you use spot instances, instances that receive interruption warnings should be drained.
267
+ # If you set URLs of SQS queues for spot instance interruption warnings to `spot_instance_intrp_warns_queue_urls`,
268
+ # autoscaler drains instances to interrupt and detaches the instances from the auto scaling groups with
269
+ # should_decrement_desired_capacity false.
270
+ # If you set ECS_ENABLE_SPOT_INSTANCE_DRAINING to true, we recommend that you opt out of the draining feature
271
+ # by setting disable_draining to true in the configurations of auto scaling groups and spot fleet requests.
272
+ # Otherwise, instances don't seem to be drained on rare occasions.
273
+ # Even if you opt out of the feature, you still have the advantage of setting `spot_instance_intrp_warns_queue_urls`
274
+ # because instances to interrupt are replaced with new instances as soon as possible.
269
275
  spot_instance_intrp_warns_queue_urls:
270
276
  - https://sqs.ap-northeast-1.amazonaws.com/<account-id>/spot-instance-intrp-warns
271
277
  ```
data/ecs_deploy.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "bundler", ">= 1.11", "< 3"
31
31
  spec.add_development_dependency "rake", ">= 10.0"
32
32
  spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_development_dependency "rexml" # For aws-sdk-*
33
34
  end
@@ -7,7 +7,7 @@ require "ecs_deploy/auto_scaler/cluster_resource_manager"
7
7
 
8
8
  module EcsDeploy
9
9
  module AutoScaler
10
- AutoScalingGroupConfig = Struct.new(:name, :region, :cluster, :buffer, :service_configs) do
10
+ AutoScalingGroupConfig = Struct.new(:name, :region, :cluster, :buffer, :service_configs, :disable_draining) do
11
11
  include ConfigBase
12
12
 
13
13
  MAX_DETACHABLE_INSTANCE_COUNT = 20
@@ -82,7 +82,7 @@ module EcsDeploy
82
82
  def decrease_desired_capacity(count)
83
83
  container_instance_arns_in_service = cluster_resource_manager.fetch_container_instance_arns_in_service
84
84
  container_instances_in_cluster = cluster_resource_manager.fetch_container_instances_in_cluster
85
- auto_scaling_group_instances = instances(reload: true)
85
+ auto_scaling_group_instances = describe_detachable_instances
86
86
  deregisterable_instances = container_instances_in_cluster.select do |i|
87
87
  i.pending_tasks_count == 0 &&
88
88
  !running_essential_task?(i, container_instance_arns_in_service) &&
@@ -144,15 +144,8 @@ module EcsDeploy
144
144
 
145
145
  def detach_and_terminate_orphan_instances
146
146
  container_instance_ids = cluster_resource_manager.fetch_container_instances_in_cluster.map(&:ec2_instance_id)
147
- orphans = instances(reload: true).reject do |i|
147
+ orphans = describe_detachable_instances.reject do |i|
148
148
  next true if container_instance_ids.include?(i.instance_id)
149
-
150
- # The lifecycle state of terminated instances becomes "Terminating", "Terminating:Wait", or "Terminating:Proceed",
151
- # and we can't detach instances in such a state.
152
- if i.lifecycle_state.start_with?("Terminating")
153
- AutoScaler.error_logger.warn("#{log_prefix} The lifesycle state of #{i.instance_id} is \"#{i.lifecycle_state}\", so ignore it")
154
- next true
155
- end
156
149
  end.map(&:instance_id)
157
150
 
158
151
  return if orphans.empty?
@@ -184,14 +177,11 @@ module EcsDeploy
184
177
  )
185
178
  end
186
179
 
187
- def instances(reload: false)
188
- if reload || @instances.nil?
189
- resp = client.describe_auto_scaling_groups({
190
- auto_scaling_group_names: [name],
191
- })
192
- @instances = resp.auto_scaling_groups[0].instances
193
- else
194
- @instances
180
+ def describe_detachable_instances
181
+ client.describe_auto_scaling_groups({ auto_scaling_group_names: [name] }).auto_scaling_groups[0].instances.reject do |i|
182
+ # The lifecycle state of terminated instances becomes "Detaching", "Terminating", "Terminating:Wait", or "Terminating:Proceed",
183
+ # and we can't detach instances in such a state.
184
+ i.lifecycle_state.start_with?("Terminating") || i.lifecycle_state == "Detaching"
195
185
  end
196
186
  end
197
187
 
@@ -74,39 +74,42 @@ module EcsDeploy
74
74
  end
75
75
 
76
76
  def trigger_capacity_update(old_desired_capacity, new_desired_capacity, interval: 5, wait_until_capacity_updated: false)
77
+ return if new_desired_capacity == old_desired_capacity
78
+
77
79
  th = Thread.new do
78
80
  @logger&.info "#{log_prefix} Start updating capacity: #{old_desired_capacity} -> #{new_desired_capacity}"
79
81
  Timeout.timeout(180) do
80
- until @capacity == new_desired_capacity || (new_desired_capacity >= old_desired_capacity && @capacity > new_desired_capacity)
82
+ until @capacity == new_desired_capacity ||
83
+ (new_desired_capacity > old_desired_capacity && @capacity > new_desired_capacity) ||
84
+ (new_desired_capacity < old_desired_capacity && @capacity < new_desired_capacity)
81
85
  @mutex.synchronize do
82
- begin
83
- @capacity = calculate_active_instance_capacity
84
- @resource.broadcast
85
- rescue => e
86
- AutoScaler.error_logger.warn("#{log_prefix} `#{__method__}': #{e} (#{e.class})")
87
- end
86
+ @capacity = calculate_active_instance_capacity
87
+ @resource.broadcast
88
+ rescue => e
89
+ AutoScaler.error_logger.warn("#{log_prefix} `#{__method__}': #{e} (#{e.class})")
88
90
  end
89
91
 
90
92
  sleep interval
91
93
  end
92
94
  @logger&.info "#{log_prefix} capacity is updated to #{@capacity}"
93
95
  end
96
+ rescue Timeout::Error => e
97
+ msg = "#{log_prefix} `#{__method__}': #{e} (#{e.class})"
98
+ if @capacity_based_on == "vCPUs"
99
+ # Timeout::Error sometimes occur.
100
+ # For example, the following case never meats the condition of until
101
+ # * old_desired_capaacity is 102
102
+ # * new_desired_capaacity is 101
103
+ # * all instances have 2 vCPUs
104
+ AutoScaler.error_logger.warn(msg)
105
+ else
106
+ AutoScaler.error_logger.error(msg)
107
+ end
94
108
  end
95
109
 
96
110
  if wait_until_capacity_updated
97
111
  @logger&.info "#{log_prefix} Wait for the capacity of active instances to become #{new_desired_capacity} from #{old_desired_capacity}"
98
- begin
99
- th.join
100
- rescue Timeout::Error => e
101
- msg = "#{log_prefix} `#{__method__}': #{e} (#{e.class})"
102
- if @capacity_based_on == "vCPUs"
103
- # Timeout::Error sometimes occur.
104
- # For example, @capacity won't be new_desired_capacity if new_desired_capacity is odd and all instances have 2 vCPUs
105
- AutoScaler.error_logger.warn(msg)
106
- else
107
- AutoScaler.error_logger.error(msg)
108
- end
109
- end
112
+ th.join
110
113
  end
111
114
  end
112
115
 
@@ -78,6 +78,11 @@ module EcsDeploy
78
78
  def set_instance_state_to_draining(config_to_instance_ids, region)
79
79
  cl = ecs_client(region)
80
80
  config_to_instance_ids.each do |config, instance_ids|
81
+ if config.disable_draining == true || config.disable_draining == "true"
82
+ @logger.info "Skip draining instances: region: #{region}, cluster: #{config.cluster}, instance_ids: #{instance_ids.inspect}"
83
+ next
84
+ end
85
+
81
86
  arns = cl.list_container_instances(
82
87
  cluster: config.cluster,
83
88
  filter: "ec2InstanceId in [#{instance_ids.join(",")}]",
@@ -135,6 +135,9 @@ module EcsDeploy
135
135
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
136
136
  @reach_max_at ||= now
137
137
  @logger.info "#{log_prefix} Service waits cooldown elapsed #{(now - @reach_max_at).to_i}sec"
138
+ if next_desired_count > max_task_count[current_level] && current_level == max_task_count.size - 1
139
+ @logger.warn "#{log_prefix} Desired count has reached the maximum value and couldn't be increased"
140
+ end
138
141
  elsif current_level == next_level && next_desired_count < max_task_count[current_level]
139
142
  level = current_level
140
143
  @reach_max_at = nil
@@ -8,7 +8,7 @@ require "ecs_deploy/auto_scaler/cluster_resource_manager"
8
8
 
9
9
  module EcsDeploy
10
10
  module AutoScaler
11
- SpotFleetRequestConfig = Struct.new(:id, :region, :cluster, :buffer, :service_configs) do
11
+ SpotFleetRequestConfig = Struct.new(:id, :region, :cluster, :buffer, :service_configs, :disable_draining) do
12
12
  include ConfigBase
13
13
 
14
14
  def initialize(attributes = {}, logger)
@@ -113,6 +113,7 @@ namespace :ecs do
113
113
  service_options[:deployment_configuration] = service[:deployment_configuration] if service[:deployment_configuration]
114
114
  service_options[:placement_constraints] = service[:placement_constraints] if service[:placement_constraints]
115
115
  service_options[:placement_strategy] = service[:placement_strategy] if service[:placement_strategy]
116
+ service_options[:capacity_provider_strategy] = service[:capacity_provider_strategy] if service[:capacity_provider_strategy]
116
117
  service_options[:scheduling_strategy] = service[:scheduling_strategy] if service[:scheduling_strategy]
117
118
  s = EcsDeploy::Service.new(**service_options)
118
119
  s.deploy
@@ -179,6 +180,7 @@ namespace :ecs do
179
180
  service_options[:deployment_configuration] = service[:deployment_configuration] if service[:deployment_configuration]
180
181
  service_options[:placement_constraints] = service[:placement_constraints] if service[:placement_constraints]
181
182
  service_options[:placement_strategy] = service[:placement_strategy] if service[:placement_strategy]
183
+ service_options[:capacity_provider_strategy] = service[:capacity_provider_strategy] if service[:capacity_provider_strategy]
182
184
  s = EcsDeploy::Service.new(**service_options)
183
185
  s.deploy
184
186
  EcsDeploy::TaskDefinition.deregister(current_task_definition_arn, region: r)
@@ -7,7 +7,7 @@ module EcsDeploy
7
7
 
8
8
  class TooManyAttemptsError < StandardError; end
9
9
 
10
- attr_reader :cluster, :region, :service_name, :delete
10
+ attr_reader :cluster, :region, :service_name, :delete, :deploy_started_at
11
11
 
12
12
  def initialize(
13
13
  cluster:, service_name:, task_definition_name: nil, revision: nil,
@@ -16,6 +16,7 @@ module EcsDeploy
16
16
  launch_type: nil,
17
17
  placement_constraints: [],
18
18
  placement_strategy: [],
19
+ capacity_provider_strategy: nil,
19
20
  network_configuration: nil,
20
21
  health_check_grace_period_seconds: nil,
21
22
  scheduling_strategy: 'REPLICA',
@@ -35,6 +36,7 @@ module EcsDeploy
35
36
  @launch_type = launch_type
36
37
  @placement_constraints = placement_constraints
37
38
  @placement_strategy = placement_strategy
39
+ @capacity_provider_strategy = capacity_provider_strategy
38
40
  @network_configuration = network_configuration
39
41
  @health_check_grace_period_seconds = health_check_grace_period_seconds
40
42
  @scheduling_strategy = scheduling_strategy
@@ -59,6 +61,7 @@ module EcsDeploy
59
61
  end
60
62
 
61
63
  def deploy
64
+ @deploy_started_at = Time.now
62
65
  res = @client.describe_services(cluster: @cluster, services: [@service_name])
63
66
  service_options = {
64
67
  cluster: @cluster,
@@ -66,8 +69,25 @@ module EcsDeploy
66
69
  deployment_configuration: @deployment_configuration,
67
70
  network_configuration: @network_configuration,
68
71
  health_check_grace_period_seconds: @health_check_grace_period_seconds,
72
+ capacity_provider_strategy: @capacity_provider_strategy,
69
73
  enable_execute_command: @enable_execute_command,
74
+ enable_ecs_managed_tags: @enable_ecs_managed_tags,
75
+ placement_constraints: @placement_constraints,
76
+ placement_strategy: @placement_strategy,
70
77
  }
78
+
79
+ if @load_balancers && EcsDeploy.config.ecs_service_role
80
+ service_options.merge!({
81
+ role: EcsDeploy.config.ecs_service_role,
82
+ })
83
+ end
84
+
85
+ if @load_balancers
86
+ service_options.merge!({
87
+ load_balancers: @load_balancers,
88
+ })
89
+ end
90
+
71
91
  if res.services.select{ |s| s.status == 'ACTIVE' }.empty?
72
92
  return if @delete
73
93
 
@@ -75,25 +95,10 @@ module EcsDeploy
75
95
  service_name: @service_name,
76
96
  desired_count: @desired_count.to_i,
77
97
  launch_type: @launch_type,
78
- placement_constraints: @placement_constraints,
79
- placement_strategy: @placement_strategy,
80
- enable_ecs_managed_tags: @enable_ecs_managed_tags,
81
98
  tags: @tags,
82
99
  propagate_tags: @propagate_tags,
83
100
  })
84
101
 
85
- if @load_balancers && EcsDeploy.config.ecs_service_role
86
- service_options.merge!({
87
- role: EcsDeploy.config.ecs_service_role,
88
- })
89
- end
90
-
91
- if @load_balancers
92
- service_options.merge!({
93
- load_balancers: @load_balancers,
94
- })
95
- end
96
-
97
102
  if @scheduling_strategy == 'DAEMON'
98
103
  service_options[:scheduling_strategy] = @scheduling_strategy
99
104
  service_options.delete(:desired_count)
@@ -105,12 +110,34 @@ module EcsDeploy
105
110
 
106
111
  service_options.merge!({service: @service_name})
107
112
  service_options.merge!({desired_count: @desired_count}) if @desired_count
113
+ service_options.merge!({propagate_tags: @propagate_tags}) if @propagate_tags
114
+
115
+ current_service = res.services[0]
116
+ service_options.merge!({force_new_deployment: true}) if need_force_new_deployment?(current_service)
117
+
108
118
  update_tags(@service_name, @tags)
109
119
  @response = @client.update_service(service_options)
110
120
  EcsDeploy.logger.info "update service [#{@service_name}] [#{@cluster}] [#{@region}] [#{Paint['OK', :green]}]"
111
121
  end
112
122
  end
113
123
 
124
+ private def need_force_new_deployment?(service)
125
+ return false unless @capacity_provider_strategy
126
+ return true unless service.capacity_provider_strategy
127
+
128
+ return true if @capacity_provider_strategy.size != service.capacity_provider_strategy.size
129
+
130
+ match_array = @capacity_provider_strategy.all? do |strategy|
131
+ service.capacity_provider_strategy.find do |current_strategy|
132
+ strategy[:capacity_provider] == current_strategy.capacity_provider &&
133
+ strategy[:weight] == current_strategy.weight &&
134
+ strategy[:base] == current_strategy.base
135
+ end
136
+ end
137
+
138
+ !match_array
139
+ end
140
+
114
141
  def delete_service
115
142
  if @scheduling_strategy != 'DAEMON'
116
143
  @client.update_service(cluster: @cluster, service: @service_name, desired_count: 0)
@@ -142,6 +169,16 @@ module EcsDeploy
142
169
  end
143
170
  end
144
171
 
172
+ def log_events(ecs_service)
173
+ ecs_service.events.sort_by(&:created_at).each do |e|
174
+ next if e.created_at <= deploy_started_at
175
+ next if @last_event && e.created_at <= @last_event.created_at
176
+
177
+ EcsDeploy.logger.info e.message
178
+ @last_event = e
179
+ end
180
+ end
181
+
145
182
  def self.wait_all_running(services)
146
183
  services.group_by { |s| [s.cluster, s.region] }.flat_map do |(cl, region), ss|
147
184
  client = Aws::ECS::Client.new(region: region)
@@ -155,6 +192,8 @@ module EcsDeploy
155
192
  if s.deployments.size == 1 && s.running_count == s.desired_count
156
193
  chunked_service_names.delete(s.service_name)
157
194
  end
195
+ service = ss.detect {|sc| sc.service_name == s.service_name }
196
+ service.log_events(s)
158
197
  end
159
198
  break if chunked_service_names.empty?
160
199
  sleep EcsDeploy.config.ecs_wait_until_services_stable_delay
@@ -1,3 +1,3 @@
1
1
  module EcsDeploy
2
- VERSION = "1.0.3"
2
+ VERSION = "1.0.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecs_deploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - joker1007
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-17 00:00:00.000000000 Z
11
+ date: 2023-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-autoscaling
@@ -170,6 +170,20 @@ dependencies:
170
170
  - - "~>"
171
171
  - !ruby/object:Gem::Version
172
172
  version: '3.0'
173
+ - !ruby/object:Gem::Dependency
174
+ name: rexml
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ type: :development
181
+ prerelease: false
182
+ version_requirements: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
173
187
  description: AWS ECS deploy helper
174
188
  email:
175
189
  - kakyoin.hierophant@gmail.com
@@ -178,8 +192,8 @@ executables:
178
192
  extensions: []
179
193
  extra_rdoc_files: []
180
194
  files:
195
+ - ".github/workflows/test.yml"
181
196
  - ".gitignore"
182
- - ".travis.yml"
183
197
  - CHANGELOG.md
184
198
  - Gemfile
185
199
  - README.md
@@ -222,7 +236,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
222
236
  - !ruby/object:Gem::Version
223
237
  version: '0'
224
238
  requirements: []
225
- rubygems_version: 3.2.15
239
+ rubygems_version: 3.4.2
226
240
  signing_key:
227
241
  specification_version: 4
228
242
  summary: AWS ECS deploy helper
data/.travis.yml DELETED
@@ -1,5 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - ruby-2.4.9
4
- - ruby-2.5.7
5
- - ruby-2.6.5