rollout 2.6.2 → 3.2.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.
data/spec/spec_helper.rb CHANGED
@@ -5,23 +5,96 @@ require 'simplecov'
5
5
  SimpleCov.start
6
6
 
7
7
  require 'bundler/setup'
8
- require 'redis'
9
8
  require 'rollout'
10
9
 
11
- $redis = Redis.new(
12
- host: ENV.fetch('REDIS_HOST', '127.0.0.1'),
13
- port: ENV.fetch('REDIS_PORT', '6379'),
14
- db: ENV.fetch('REDIS_DB', '7'),
15
- )
10
+ class RolloutMemoryBackend
11
+ def initialize
12
+ @features = {}
13
+ @events = Hash.new { |hash, key| hash[key] = [] }
14
+ @global_events = []
15
+ end
16
+
17
+ def fetch_feature(name)
18
+ state = @features[name.to_s]
19
+ return Rollout::FeatureState.new(name: name, percentage: 0) unless state
20
+
21
+ state.deep_clone
22
+ end
23
+
24
+ def fetch_features(names)
25
+ names.map { |name| fetch_feature(name) }
26
+ end
27
+
28
+ def feature_names
29
+ @features.keys
30
+ end
31
+
32
+ def feature_exists?(name)
33
+ @features.key?(name.to_s)
34
+ end
35
+
36
+ def save_feature(state)
37
+ @features[state.name] = state.deep_clone
38
+ end
39
+
40
+ def delete_feature(name)
41
+ @features.delete(name.to_s)
42
+ end
43
+
44
+ def clear_features
45
+ end
46
+
47
+ def mutate_feature(name)
48
+ mutation = yield fetch_feature(name)
49
+ save_feature(mutation.fetch(:state))
50
+ event = mutation[:event]
51
+ if event
52
+ @events[name.to_s] << event
53
+ @global_events << event if mutation[:global]
54
+ end
55
+ mutation
56
+ end
57
+
58
+ def feature_events(name, limit: nil)
59
+ limited_events(@events[name.to_s], limit)
60
+ end
61
+
62
+ def global_events(limit: nil)
63
+ limited_events(@global_events, limit)
64
+ end
65
+
66
+ def feature_updated_at(_name)
67
+ end
68
+
69
+ def delete_feature_events(name)
70
+ @events.delete(name.to_s)
71
+ end
72
+
73
+ def delete_feature_with_history(name, event:, history_length:)
74
+ existed = feature_exists?(name)
75
+ delete_feature(name)
76
+ delete_feature_events(name)
77
+ if existed && event
78
+ @global_events << event
79
+ @global_events = @global_events.last(history_length)
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def limited_events(events, limit)
86
+ return events if limit.nil?
87
+ raise ArgumentError, "limit must be an Integer" unless limit.is_a?(Integer)
88
+ raise ArgumentError, "limit must be >= 0" if limit < 0
89
+
90
+ events.last(limit)
91
+ end
92
+ end
16
93
 
17
94
  RSpec.configure do |config|
18
95
  config.example_status_persistence_file_path = '.rspec_status'
19
96
 
20
- # config.disable_monkey_patching!
21
-
22
97
  config.expect_with :rspec do |c|
23
98
  c.syntax = :expect
24
99
  end
25
-
26
- config.before { $redis.flushdb }
27
100
  end
@@ -0,0 +1,95 @@
1
+ require "date"
2
+
3
+ RSpec.shared_examples "a rollout feature backend" do
4
+ def empty_state(name)
5
+ Rollout::FeatureState.new(name: name, percentage: 0)
6
+ end
7
+
8
+ def json_metadata
9
+ {
10
+ "description" => "New navigation",
11
+ "updated_at" => 1,
12
+ "enabled" => true,
13
+ "ratio" => 0.5,
14
+ "owner" => nil,
15
+ "label" => :beta,
16
+ "released_at" => Time.utc(2026, 1, 1),
17
+ "day" => Date.new(2026, 1, 1),
18
+ "labels" => ["a", { "nested" => :ok }],
19
+ }
20
+ end
21
+
22
+ it "returns an empty state for a missing feature" do
23
+ state = backend.fetch_feature(:chat)
24
+
25
+ expect(state).to eq empty_state(:chat)
26
+ end
27
+
28
+ it "preserves fetch_features order and duplicates" do
29
+ backend.save_feature(Rollout::FeatureState.new(name: :chat, percentage: 25))
30
+
31
+ states = backend.fetch_features([:chat, :missing, :chat])
32
+
33
+ expect(states.map(&:name)).to eq %w[chat missing chat]
34
+ expect(states.map(&:percentage)).to eq [25.0, 0.0, 25.0]
35
+ end
36
+
37
+ it "returns no features for an empty fetch_features request" do
38
+ expect(backend.fetch_features([])).to eq []
39
+ end
40
+
41
+ it "round-trips a double-precision percentage" do
42
+ percentage = 33.333333333333336
43
+ backend.save_feature(Rollout::FeatureState.new(name: :chat, percentage: percentage))
44
+
45
+ expect(backend.fetch_feature(:chat).percentage).to eq percentage
46
+ end
47
+
48
+ it "does not share nested data with a fetched state" do
49
+ backend.save_feature(
50
+ Rollout::FeatureState.new(name: :chat, percentage: 0, data: { "labels" => ["a"] }),
51
+ )
52
+
53
+ backend.fetch_feature(:chat).data["labels"] << "b"
54
+
55
+ expect(backend.fetch_feature(:chat).data).to eq("labels" => ["a"])
56
+ end
57
+
58
+ it "does not share nested data with a saved state" do
59
+ state = Rollout::FeatureState.new(name: :chat, percentage: 0, data: { "labels" => ["a"] })
60
+ backend.save_feature(state)
61
+
62
+ state.data["labels"] << "b"
63
+
64
+ expect(backend.fetch_feature(:chat).data).to eq("labels" => ["a"])
65
+ end
66
+
67
+ it "returns the same canonical metadata after save_feature" do
68
+ state = Rollout::FeatureState.new(name: :chat, percentage: 0, data: json_metadata)
69
+ backend.save_feature(state)
70
+
71
+ expect(backend.fetch_feature(:chat)).to eq state
72
+ expect(state.data["label"]).to eq "beta"
73
+ expect(state.data["labels"]).to eq ["a", { "nested" => "ok" }]
74
+ end
75
+
76
+ it "tracks existence and names after save and delete" do
77
+ backend.save_feature(Rollout::FeatureState.new(name: :chat, percentage: 25))
78
+
79
+ expect(backend.feature_exists?(:chat)).to be_truthy
80
+ expect(backend.feature_names).to eq ["chat"]
81
+
82
+ backend.delete_feature(:chat)
83
+
84
+ expect(backend.feature_exists?(:chat)).to be_falsey
85
+ expect(backend.fetch_feature(:chat)).to eq empty_state(:chat)
86
+ end
87
+
88
+ it "does not persist when mutate_feature's block raises" do
89
+ expect {
90
+ backend.mutate_feature(:chat) { raise "nope" }
91
+ }.to raise_error("nope")
92
+
93
+ expect(backend.feature_exists?(:chat)).to be_falsey
94
+ end
95
+ end
@@ -0,0 +1,174 @@
1
+ RSpec.shared_examples "a rollout history backend" do
2
+ let(:rollout) { Rollout.new(adapter: backend, logging: logging) }
3
+ let(:logging) { true }
4
+ let(:feature) { :foo }
5
+
6
+ it "persists feature events oldest-to-newest" do
7
+ rollout.activate_percentage(feature, 50)
8
+ rollout.activate_percentage(feature, 75)
9
+ rollout.activate_group(feature, :hipsters)
10
+
11
+ events = rollout.logging.events(feature)
12
+ expect(events.map(&:name)).to eq %w[update update update]
13
+ expect(events.map { |event| event.data }).to eq [
14
+ { before: { percentage: 0 }, after: { percentage: 50 } },
15
+ { before: { percentage: 50 }, after: { percentage: 75 } },
16
+ { before: { groups: [] }, after: { groups: ["hipsters"] } },
17
+ ]
18
+ expect(rollout.logging.updated_at(feature)).to_not be_nil
19
+ end
20
+
21
+ it "does not write a history event when a double-precision percentage is unchanged" do
22
+ percentage = 33.333333333333336
23
+ rollout.activate_percentage(feature, percentage)
24
+
25
+ expect do
26
+ rollout.activate_percentage(feature, percentage)
27
+ end.not_to change { rollout.logging.events(feature).count }
28
+
29
+ expect(rollout.get(feature).percentage).to eq percentage
30
+ end
31
+
32
+ it "persists metadata changes and actor context" do
33
+ rollout.logging.with_context(actor: "alice") do
34
+ rollout.with_feature(feature) do |current|
35
+ current.percentage = 25.0
36
+ current.data.update(description: "New navigation")
37
+ end
38
+ end
39
+
40
+ event = rollout.logging.last_event(feature)
41
+ expect(event.data).to eq(
42
+ before: { percentage: 0, "data.description": nil },
43
+ after: { percentage: 25.0, "data.description": "New navigation" },
44
+ )
45
+ expect(event.context).to eq(actor: "alice")
46
+ expect(rollout.logging.events(feature).count).to eq 1
47
+ end
48
+
49
+ context "history truncation" do
50
+ let(:logging) { { history_length: 1 } }
51
+
52
+ it "keeps only the configured number of events" do
53
+ rollout.activate_percentage(feature, 25)
54
+ rollout.activate_percentage(feature, 30)
55
+
56
+ expect(rollout.logging.events(feature).map { |event| event.data[:after][:percentage] }).to eq [30]
57
+ end
58
+ end
59
+
60
+ context "global logs" do
61
+ let(:logging) { { global: true } }
62
+
63
+ it "logs changes across features oldest-to-newest" do
64
+ rollout.activate_percentage("foo", 25)
65
+ rollout.activate_percentage("bar", 30)
66
+ rollout.activate_percentage("baz", 40)
67
+
68
+ expect(rollout.logging.global_events.map(&:feature)).to eq %w[foo bar baz]
69
+ expect(rollout.logging.global_events(limit: 2).map(&:feature)).to eq %w[bar baz]
70
+ end
71
+ end
72
+
73
+ it "removes feature history on delete" do
74
+ rollout.activate_percentage(feature, 25)
75
+ rollout.delete(feature)
76
+
77
+ expect(rollout.logging.events(feature)).to eq []
78
+ end
79
+
80
+ it "records a global-only delete event for an existing feature" do
81
+ logged_rollout = Rollout.new(adapter: backend, logging: { global: true, history_length: 2 })
82
+ logged_rollout.activate_percentage(feature, 25)
83
+ logged_rollout.logging.with_context(actor: "alice") { logged_rollout.delete(feature) }
84
+
85
+ event = logged_rollout.logging.global_events.last
86
+ expect(event.name).to eq "delete"
87
+ expect(event.feature).to eq feature.to_s
88
+ expect(event.data).to eq({})
89
+ expect(event.context).to eq(actor: "alice")
90
+ expect(logged_rollout.logging.events(feature)).to eq []
91
+ expect(logged_rollout.logging.updated_at(feature)).to be_nil
92
+ expect(backend.feature_names.map(&:to_s)).not_to include(feature.to_s)
93
+ end
94
+
95
+ it "does not record deletions of missing features and honors global history length" do
96
+ logged_rollout = Rollout.new(adapter: backend, logging: { global: true, history_length: 1 })
97
+ logged_rollout.activate_percentage(feature, 25)
98
+ logged_rollout.delete(feature)
99
+ expect(logged_rollout.logging.global_events.map(&:name)).to eq ["delete"]
100
+
101
+ logged_rollout.delete(feature)
102
+
103
+ expect(logged_rollout.logging.global_events.map(&:name)).to eq ["delete"]
104
+ end
105
+
106
+ it "does not add a delete event when global history is disabled" do
107
+ logged_rollout = Rollout.new(adapter: backend, logging: { global: false })
108
+ logged_rollout.activate_percentage(feature, 25)
109
+
110
+ logged_rollout.delete(feature)
111
+
112
+ expect(logged_rollout.logging.global_events).to eq []
113
+ expect(logged_rollout.logging.events(feature)).to eq []
114
+ end
115
+
116
+ it "does not retain a delete event when history length is zero" do
117
+ logged_rollout = Rollout.new(adapter: backend, logging: { global: true, history_length: 0 })
118
+ logged_rollout.activate_percentage(feature, 25)
119
+
120
+ logged_rollout.delete(feature)
121
+
122
+ expect(logged_rollout.logging.global_events).to eq []
123
+ expect(logged_rollout.logging.events(feature)).to eq []
124
+ end
125
+
126
+ it "keeps feature history on clear!" do
127
+ rollout.activate_percentage(feature, 25)
128
+ rollout.clear!
129
+
130
+ expect(rollout.features).to eq []
131
+ expect(rollout.logging.events(feature).map { |event| event.data[:after][:percentage] }).to eq [25, 0]
132
+ end
133
+
134
+ it "keeps feature history when deleting without logging" do
135
+ rollout.activate_percentage(feature, 25)
136
+
137
+ Rollout.new(adapter: backend).delete(feature)
138
+
139
+ expect(rollout.exists?(feature)).to be_falsey
140
+ expect(rollout.logging.events(feature)).not_to eq []
141
+ end
142
+
143
+ it "logging.delete removes feature history only" do
144
+ rollout.activate_percentage(feature, 25)
145
+ rollout.logging.delete(feature)
146
+
147
+ expect(rollout.logging.events(feature)).to eq []
148
+ expect(rollout.logging.updated_at(feature)).to be_nil
149
+ expect(rollout.get(feature).percentage).to eq 25
150
+ end
151
+
152
+ it "adapter delete_feature preserves history" do
153
+ rollout.activate_percentage(feature, 25)
154
+ rollout.adapter.delete_feature(feature)
155
+
156
+ expect(rollout.exists?(feature)).to be_falsey
157
+ expect(rollout.logging.events(feature)).not_to eq []
158
+ end
159
+
160
+ it "returns the newest events oldest-to-newest when limited" do
161
+ rollout.activate_percentage(feature, 25)
162
+ rollout.activate_percentage(feature, 50)
163
+ rollout.activate_percentage(feature, 75)
164
+
165
+ expect(rollout.logging.events(feature, limit: 2).map { |event| event.data[:after][:percentage] }).to eq [50, 75]
166
+ expect(rollout.logging.events(feature, limit: 0)).to eq []
167
+ expect(rollout.logging.last_event(feature).data[:after][:percentage]).to eq 75
168
+ end
169
+
170
+ it "rejects an invalid history limit" do
171
+ expect { rollout.logging.events(feature, limit: -1) }.to raise_error(ArgumentError)
172
+ expect { rollout.adapter.feature_events(feature, limit: 1.5) }.to raise_error(ArgumentError)
173
+ end
174
+ end
@@ -0,0 +1,77 @@
1
+ RSpec.shared_examples "a rollout backend integration" do
2
+ let(:rollout) { Rollout.new(adapter: backend) }
3
+
4
+ it "persists combined feature state" do
5
+ rollout.with_feature(:chat) do |feature|
6
+ feature.percentage = 10.5
7
+ feature.groups = [:employees]
8
+ feature.users = ["123"]
9
+ feature.data.update(description: "New navigation", label: :beta)
10
+ end
11
+
12
+ feature = rollout.get(:chat)
13
+ expect(feature.percentage).to eq 10.5
14
+ expect(feature.groups).to eq [:employees]
15
+ expect(feature.users).to eq %w[123]
16
+ expect(feature.data).to eq("description" => "New navigation", "label" => "beta")
17
+ expect(rollout.exists?(:chat)).to eq true
18
+ expect(rollout.features).to eq [:chat]
19
+ end
20
+
21
+ it "persists metadata that contains pipes" do
22
+ rollout.activate_user(:chat, 8)
23
+ rollout.set_feature_data(:chat, "|call||text|" => "a|bunch|of|stuff")
24
+
25
+ expect(rollout.get(:chat).data).to include("|call||text|" => "a|bunch|of|stuff")
26
+ expect(rollout.get(:chat).users).to eq %w[8]
27
+ end
28
+
29
+ it "clears users, groups, percentage, and data on deactivate" do
30
+ rollout.activate_user(:chat, 42)
31
+ rollout.activate_group(:chat, :employees)
32
+ rollout.activate_percentage(:chat, 50)
33
+ rollout.set_feature_data(:chat, description: "foo")
34
+
35
+ rollout.deactivate(:chat)
36
+
37
+ expect(rollout.features).to eq [:chat]
38
+ expect(rollout.get(:chat).to_hash).to eq(
39
+ percentage: 0,
40
+ users: [],
41
+ groups: [],
42
+ data: {},
43
+ )
44
+ end
45
+
46
+ it "keeps users, groups, and data on deactivate_percentage" do
47
+ rollout.activate_user(:chat, 42)
48
+ rollout.activate_group(:chat, :employees)
49
+ rollout.activate_percentage(:chat, 50)
50
+ rollout.set_feature_data(:chat, description: "foo")
51
+
52
+ rollout.deactivate_percentage(:chat)
53
+
54
+ expect(rollout.get(:chat).percentage).to eq 0
55
+ expect(rollout.get(:chat).users).to eq %w[42]
56
+ expect(rollout.get(:chat).groups).to eq [:employees]
57
+ expect(rollout.get(:chat).data).to eq("description" => "foo")
58
+ end
59
+
60
+ it "removes the feature on delete" do
61
+ rollout.activate(:chat)
62
+ rollout.delete(:chat)
63
+
64
+ expect(rollout.features).to eq []
65
+ expect(rollout.exists?(:chat)).to eq false
66
+ expect(rollout.get(:chat).percentage).to eq 0
67
+ end
68
+
69
+ it "clears persisted features" do
70
+ rollout.activate(:signup)
71
+ rollout.activate(:chat)
72
+ rollout.clear!
73
+
74
+ expect(rollout.features).to eq []
75
+ expect(rollout.exists?(:chat)).to eq false
76
+ end
77
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rollout
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.2
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Golick
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-06 00:00:00.000000000 Z
11
+ date: 2026-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: observer
@@ -24,26 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: redis
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '4.0'
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '6'
37
- type: :runtime
38
- prerelease: false
39
- version_requirements: !ruby/object:Gem::Requirement
40
- requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- version: '4.0'
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '6'
47
27
  - !ruby/object:Gem::Dependency
48
28
  name: rake
49
29
  requirement: !ruby/object:Gem::Requirement
@@ -64,14 +44,14 @@ dependencies:
64
44
  requirements:
65
45
  - - ">="
66
46
  - !ruby/object:Gem::Version
67
- version: '1.17'
47
+ version: '2.1'
68
48
  type: :development
69
49
  prerelease: false
70
50
  version_requirements: !ruby/object:Gem::Requirement
71
51
  requirements:
72
52
  - - ">="
73
53
  - !ruby/object:Gem::Version
74
- version: '1.17'
54
+ version: '2.1'
75
55
  - !ruby/object:Gem::Dependency
76
56
  name: pry
77
57
  requirement: !ruby/object:Gem::Requirement
@@ -142,14 +122,13 @@ dependencies:
142
122
  - - '='
143
123
  - !ruby/object:Gem::Version
144
124
  version: '0.17'
145
- description: Feature flippers with redis.
125
+ description: Feature flippers.
146
126
  email:
147
127
  - jamesgolick@gmail.com
148
128
  executables: []
149
129
  extensions: []
150
130
  extra_rdoc_files: []
151
131
  files:
152
- - ".circleci/config.yml"
153
132
  - ".github/workflows/release.yml"
154
133
  - ".github/workflows/test.yml"
155
134
  - ".gitignore"
@@ -161,15 +140,22 @@ files:
161
140
  - LICENSE
162
141
  - README.md
163
142
  - Rakefile
143
+ - docs/upgrading-to-v3.md
164
144
  - lib/rollout.rb
165
145
  - lib/rollout/feature.rb
146
+ - lib/rollout/feature_state.rb
166
147
  - lib/rollout/logging.rb
167
148
  - lib/rollout/version.rb
168
149
  - rollout.gemspec
169
150
  - spec/rollout/feature_spec.rb
151
+ - spec/rollout/feature_state_spec.rb
170
152
  - spec/rollout/logging_spec.rb
153
+ - spec/rollout/memory_backend_contract_spec.rb
171
154
  - spec/rollout_spec.rb
172
155
  - spec/spec_helper.rb
156
+ - spec/support/backend_contract.rb
157
+ - spec/support/history_contract.rb
158
+ - spec/support/rollout_backend_integration.rb
173
159
  homepage: https://github.com/FetLife/rollout
174
160
  licenses:
175
161
  - MIT
@@ -189,9 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
175
  - !ruby/object:Gem::Version
190
176
  version: '0'
191
177
  requirements: []
192
- rubyforge_project:
193
- rubygems_version: 2.5.1
178
+ rubygems_version: 3.5.22
194
179
  signing_key:
195
180
  specification_version: 4
196
- summary: Feature flippers with redis.
181
+ summary: Feature flippers.
197
182
  test_files: []
data/.circleci/config.yml DELETED
@@ -1,119 +0,0 @@
1
- version: 2.1
2
-
3
- workflows:
4
- main:
5
- jobs:
6
- - ruby33
7
- - ruby31
8
- - ruby32
9
- - ruby30
10
- - ruby27
11
- - ruby26
12
- - ruby25
13
- - ruby24
14
-
15
- executors:
16
- ruby33:
17
- docker:
18
- - image: cimg/ruby:3.3
19
- - image: cimg/redis:7.2
20
- ruby32:
21
- docker:
22
- - image: cimg/ruby:3.2
23
- - image: cimg/redis:7.2
24
- ruby31:
25
- docker:
26
- - image: cimg/ruby:3.1
27
- - image: cimg/redis:7.2
28
- ruby30:
29
- docker:
30
- - image: cimg/ruby:3.0
31
- - image: cimg/redis:7.2
32
- ruby27:
33
- docker:
34
- - image: cimg/ruby:2.7
35
- - image: cimg/redis:7.2
36
- ruby26:
37
- docker:
38
- - image: cimg/ruby:2.7
39
- - image: cimg/redis:7.2
40
- ruby25:
41
- docker:
42
- - image: cimg/ruby:2.7
43
- - image: cimg/redis:7.2
44
- ruby24:
45
- docker:
46
- - image: cimg/ruby:2.4
47
- - image: cimg/redis:7.2
48
-
49
- commands:
50
- test:
51
- steps:
52
- - run:
53
- name: Bundle Install
54
- command: bundle check --path vendor/bundle || bundle install
55
-
56
- - run:
57
- name: Run rspec
58
- command: |
59
- bundle exec rspec --format documentation --format RspecJunitFormatter --out test_results/rspec.xml
60
-
61
- jobs:
62
- ruby33:
63
- executor: ruby33
64
- steps:
65
- - checkout
66
- - test
67
-
68
- - run:
69
- name: Report Test Coverage
70
- command: |
71
- wget https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 -O cc-test-reporter
72
- chmod +x cc-test-reporter
73
- ./cc-test-reporter format-coverage -t simplecov -o coverage/codeclimate.json coverage/.resultset.json
74
- ./cc-test-reporter upload-coverage -i coverage/codeclimate.json
75
-
76
- - store_test_results:
77
- path: test_results
78
-
79
- ruby32:
80
- executor: ruby30
81
- steps:
82
- - checkout
83
- - test
84
-
85
- ruby31:
86
- executor: ruby30
87
- steps:
88
- - checkout
89
- - test
90
-
91
- ruby30:
92
- executor: ruby30
93
- steps:
94
- - checkout
95
- - test
96
-
97
- ruby27:
98
- executor: ruby27
99
- steps:
100
- - checkout
101
- - test
102
-
103
- ruby26:
104
- executor: ruby27
105
- steps:
106
- - checkout
107
- - test
108
-
109
- ruby25:
110
- executor: ruby27
111
- steps:
112
- - checkout
113
- - test
114
-
115
- ruby24:
116
- executor: ruby24
117
- steps:
118
- - checkout
119
- - test