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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: fbabd7f8b0651b97e5ebe8a2bdc4b990c810e162
4
- data.tar.gz: 4b5f9ff84fbcbbbef81e05c885b67cf699309e47
2
+ SHA256:
3
+ metadata.gz: cadea20a7d731c5538e5d6b6b19b34075843230fd5c6d75ffc4c269ce667a63f
4
+ data.tar.gz: '06483d373ff87c4e0537ab73d57be5bba089d43a4abacd87168bdf338cb4ca5e'
5
5
  SHA512:
6
- metadata.gz: 1a87eb9c45aef7a24868361e04f5bdb53fb55ab2949fb411a3f39198ddac4085b63d05a730f06820feaed797327772f4bf95fc783f8f5d5c55dcf24379c51d99
7
- data.tar.gz: 7c7e3b692bf11cbcebd99ad2a8d3e4db41a2b233ac5ce5ae0551c15f522e93d0f474e3fc5f868b3fedd76d1d8fb8751d178ac39ef3721b10e9a99b2499e3ec32
6
+ metadata.gz: 515d4d646e3c4c2c2fee20d77088a9dd22fe5730dd605764abfbc0e189591f7eaaf076830940358500e05eed30f456b9389f8843bad1df283d01b795eae6e935
7
+ data.tar.gz: c41503b0e1acff3c3c99de9b08d7c06167f2d4a2f7c7fc493230c11c70f633dcae0bc4c5a7028d04e11b01dfd7edfb6eaaf6d33255c559cd4b93fda206e10427
@@ -3,44 +3,70 @@ name: Release
3
3
  on:
4
4
  push:
5
5
  tags:
6
- - 'v*'
6
+ - 'rollout/v*'
7
+ - 'rollout-redis-adapter/v*'
8
+ - 'rollout-active_record-adapter/v*'
9
+
10
+ permissions:
11
+ contents: write
12
+ id-token: write
7
13
 
8
14
  jobs:
9
- release:
15
+ publish:
10
16
  runs-on: ubuntu-latest
11
- services:
12
- redis:
13
- image: redis:7-alpine
14
- ports:
15
- - 6379:6379
16
- options: >-
17
- --health-cmd "redis-cli ping"
18
- --health-interval 10s
19
- --health-timeout 5s
20
- --health-retries 5
17
+ outputs:
18
+ name: ${{ steps.package.outputs.name }}
19
+ version: ${{ steps.package.outputs.version }}
20
+ previous_tag: ${{ steps.package.outputs.previous_tag }}
21
21
  steps:
22
22
  - name: Checkout code
23
- uses: actions/checkout@v4
23
+ uses: actions/checkout@v5
24
+ with:
25
+ fetch-depth: 0
24
26
  - name: Setup Ruby
25
27
  uses: ruby/setup-ruby@v1
26
28
  with:
27
- bundler-cache: true
28
- - name: Run tests
29
- run: bundle exec rspec
29
+ ruby-version: '3.3'
30
+ - name: Select package
31
+ id: package
32
+ run: |
33
+ case "$GITHUB_REF_NAME" in
34
+ rollout/v*) name=rollout; directory=. ;;
35
+ rollout-redis-adapter/v*) name=rollout-redis-adapter; directory=rollout-redis-adapter ;;
36
+ rollout-active_record-adapter/v*) name=rollout-active_record-adapter; directory=rollout-active_record-adapter ;;
37
+ *) echo "Unexpected tag $GITHUB_REF_NAME" >&2; exit 1 ;;
38
+ esac
39
+ version="${GITHUB_REF_NAME##*/v}"
40
+ spec_version=$(ruby -e 'puts Gem::Specification.load(ARGV[0]).version' "$directory/$name.gemspec")
41
+ if [ "$spec_version" != "$version" ]; then
42
+ echo "Tag version $version does not match gemspec $spec_version" >&2
43
+ exit 1
44
+ fi
45
+ previous_tag=$(git tag --list "$name/v*" --sort=-version:refname | awk -v current="$GITHUB_REF_NAME" '$0 != current { print; exit }')
46
+ previous_tag=${previous_tag:-v2.6.2}
47
+ echo "name=$name" >> "$GITHUB_OUTPUT"
48
+ echo "version=$version" >> "$GITHUB_OUTPUT"
49
+ echo "directory=$directory" >> "$GITHUB_OUTPUT"
50
+ echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT"
51
+ - name: Build selected gem
52
+ working-directory: ${{ steps.package.outputs.directory }}
53
+ run: gem build "${{ steps.package.outputs.name }}.gemspec"
54
+ - name: Configure RubyGems credentials
55
+ uses: rubygems/configure-rubygems-credentials@v2.1.0
56
+ - name: Publish selected gem
57
+ working-directory: ${{ steps.package.outputs.directory }}
58
+ run: gem push "${{ steps.package.outputs.name }}-${{ steps.package.outputs.version }}.gem"
59
+
60
+ github-release:
61
+ needs: publish
62
+ runs-on: ubuntu-latest
63
+ steps:
30
64
  - name: Create GitHub Release
31
- uses: softprops/action-gh-release@v2
65
+ uses: softprops/action-gh-release@v3
32
66
  with:
33
67
  tag_name: ${{ github.ref }}
34
- name: ${{ github.ref_name }}
68
+ name: ${{ needs.publish.outputs.name }} ${{ needs.publish.outputs.version }}
35
69
  generate_release_notes: true
70
+ previous_tag: ${{ needs.publish.outputs.previous_tag }}
36
71
  draft: false
37
- prerelease: false
38
- - name: Set up RubyGems credentials
39
- env:
40
- RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
41
- run: |
42
- mkdir -p ~/.gem
43
- echo ":rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials
44
- chmod 0600 ~/.gem/credentials
45
- - name: Release to RubyGems
46
- run: bundle exec rake release
72
+ prerelease: false
@@ -4,13 +4,115 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - master
7
+ - v3
7
8
  pull_request:
8
- branches:
9
- - master
9
+
10
10
 
11
11
  jobs:
12
- test:
12
+ core:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ ruby-version: ['3.3', '3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4']
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v5
20
+ - name: Setup Ruby
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby-version }}
24
+ bundler-cache: true
25
+ - name: Run core tests
26
+ run: |
27
+ mkdir -p test_results
28
+ bundle exec rspec spec --format progress --format RspecJunitFormatter --out test_results/rspec.xml
29
+ - name: Upload test results
30
+ if: always()
31
+ uses: actions/upload-artifact@v6
32
+ with:
33
+ name: core-test-results-${{ matrix.ruby-version }}
34
+ path: test_results/rspec.xml
35
+ retention-days: 30
36
+
37
+ redis:
38
+ runs-on: ubuntu-latest
39
+ strategy:
40
+ matrix:
41
+ ruby-version: ['3.3', '3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4']
42
+ services:
43
+ redis:
44
+ image: redis:7-alpine
45
+ ports:
46
+ - 6379:6379
47
+ options: >-
48
+ --health-cmd "redis-cli ping"
49
+ --health-interval 10s
50
+ --health-timeout 5s
51
+ --health-retries 5
52
+ steps:
53
+ - name: Checkout code
54
+ uses: actions/checkout@v5
55
+ - name: Setup Ruby
56
+ uses: ruby/setup-ruby@v1
57
+ with:
58
+ ruby-version: ${{ matrix.ruby-version }}
59
+ working-directory: rollout-redis-adapter
60
+ bundler-cache: true
61
+ - name: Run Redis adapter tests
62
+ working-directory: rollout-redis-adapter
63
+ run: |
64
+ mkdir -p test_results
65
+ bundle exec rspec --format progress --format RspecJunitFormatter --out test_results/rspec.xml
66
+ - name: Upload test results
67
+ if: always()
68
+ uses: actions/upload-artifact@v6
69
+ with:
70
+ name: redis-test-results-${{ matrix.ruby-version }}
71
+ path: rollout-redis-adapter/test_results/rspec.xml
72
+ retention-days: 30
73
+
74
+ active_record:
13
75
  runs-on: ubuntu-latest
76
+ strategy:
77
+ fail-fast: false
78
+ matrix:
79
+ include:
80
+ - ruby-version: '3.3'
81
+ activerecord: '~> 8.1.0'
82
+ db: sqlite3
83
+ - ruby-version: '3.3'
84
+ activerecord: '~> 8.1.0'
85
+ db: postgresql
86
+ - ruby-version: '3.3'
87
+ activerecord: '~> 8.1.0'
88
+ db: mysql2
89
+ - ruby-version: '3.3'
90
+ activerecord: '~> 7.1.0'
91
+ db: postgresql
92
+ - ruby-version: '3.3'
93
+ activerecord: '~> 7.1.0'
94
+ db: mysql2
95
+ - ruby-version: '3.2'
96
+ activerecord: '~> 8.0.0'
97
+ db: sqlite3
98
+ - ruby-version: '3.1'
99
+ activerecord: '~> 7.2.0'
100
+ db: sqlite3
101
+ - ruby-version: '2.7'
102
+ activerecord: '~> 7.1.0'
103
+ db: sqlite3
104
+ env:
105
+ ACTIVERECORD_VERSION: ${{ matrix.activerecord }}
106
+ ROLLOUT_AR_ADAPTER: ${{ matrix.db }}
107
+ POSTGRES_HOST: localhost
108
+ POSTGRES_USER: postgres
109
+ POSTGRES_PASSWORD: postgres
110
+ POSTGRES_DB: rollout_test
111
+ MYSQL_HOST: 127.0.0.1
112
+ MYSQL_USER: root
113
+ MYSQL_PASSWORD: root
114
+ MYSQL_DATABASE: rollout_test
115
+ REDIS_HOST: 127.0.0.1
14
116
  services:
15
117
  redis:
16
118
  image: redis:7-alpine
@@ -21,12 +123,52 @@ jobs:
21
123
  --health-interval 10s
22
124
  --health-timeout 5s
23
125
  --health-retries 5
126
+ postgres:
127
+ image: postgres:16
128
+ env:
129
+ POSTGRES_USER: postgres
130
+ POSTGRES_PASSWORD: postgres
131
+ POSTGRES_DB: rollout_test
132
+ ports:
133
+ - 5432:5432
134
+ options: >-
135
+ --health-cmd "pg_isready -U postgres"
136
+ --health-interval 10s
137
+ --health-timeout 5s
138
+ --health-retries 5
139
+ mysql:
140
+ image: mysql:8.0
141
+ env:
142
+ MYSQL_ROOT_PASSWORD: root
143
+ MYSQL_DATABASE: rollout_test
144
+ ports:
145
+ - 3306:3306
146
+ options: >-
147
+ --health-cmd "mysqladmin ping -h 127.0.0.1 -uroot -proot"
148
+ --health-interval 10s
149
+ --health-timeout 5s
150
+ --health-retries 5
24
151
  steps:
25
152
  - name: Checkout code
26
- uses: actions/checkout@v4
153
+ uses: actions/checkout@v5
154
+ - name: Install database client libraries
155
+ run: sudo apt-get update && sudo apt-get install -y libpq-dev default-libmysqlclient-dev
27
156
  - name: Setup Ruby
28
157
  uses: ruby/setup-ruby@v1
29
158
  with:
159
+ ruby-version: ${{ matrix.ruby-version }}
160
+ working-directory: rollout-active_record-adapter
30
161
  bundler-cache: true
31
- - name: Run tests
32
- run: bundle exec rspec
162
+ cache-version: ar-${{ matrix.ruby-version }}-${{ matrix.activerecord }}-${{ matrix.db }}
163
+ - name: Run Active Record adapter tests
164
+ working-directory: rollout-active_record-adapter
165
+ run: |
166
+ mkdir -p test_results
167
+ bundle exec rspec --format progress --format RspecJunitFormatter --out test_results/rspec.xml
168
+ - name: Upload test results
169
+ if: always()
170
+ uses: actions/upload-artifact@v5
171
+ with:
172
+ name: active-record-test-results-${{ matrix.ruby-version }}-${{ matrix.db }}-${{ strategy.job-index }}
173
+ path: rollout-active_record-adapter/test_results/rspec.xml
174
+ retention-days: 30
data/.gitignore CHANGED
@@ -18,7 +18,9 @@ coverage
18
18
  rdoc
19
19
  pkg
20
20
  *.gem
21
+ Gemfile.lock
21
22
  gemfiles/*.lock
23
+ test_results
22
24
  .rspec_status
23
25
  .bundle/config
24
26
  /vendor
data/README.md CHANGED
@@ -1,34 +1,43 @@
1
1
  # rollout
2
2
 
3
- Fast feature flags based on Redis.
3
+ Fast feature flags.
4
+
5
+ Upgrading from Rollout 2? Follow the [Rollout 3 upgrade guide](docs/upgrading-to-v3.md)
6
+ before updating your dependencies.
4
7
 
5
8
  [![Gem Version](https://badge.fury.io/rb/rollout.svg)](https://badge.fury.io/rb/rollout)
6
- [![CircleCI](https://circleci.com/gh/fetlife/rollout.svg?style=svg)](https://circleci.com/gh/fetlife/rollout)
9
+ [![CI](https://github.com/fetlife/rollout/actions/workflows/test.yml/badge.svg)](https://github.com/fetlife/rollout/actions/workflows/test.yml)
7
10
  [![Code Climate](https://codeclimate.com/github/FetLife/rollout/badges/gpa.svg)](https://codeclimate.com/github/FetLife/rollout)
8
11
  [![Test Coverage](https://codeclimate.com/github/FetLife/rollout/badges/coverage.svg)](https://codeclimate.com/github/FetLife/rollout/coverage)
9
12
 
10
13
  ## Install it
11
14
 
12
15
  ```bash
13
- gem install rollout
16
+ gem install rollout -v '~> 3.1'
17
+ gem install rollout-redis-adapter -v '~> 0.1'
14
18
  ```
15
19
 
20
+ ```ruby
21
+ gem "rollout", "~> 3.1"
22
+ gem "rollout-redis-adapter", "~> 0.1"
23
+ ```
24
+
25
+ Active Record applications can use `rollout-active_record-adapter` instead of
26
+ `rollout-redis-adapter`. See the [Active Record adapter README](rollout-active_record-adapter/README.md).
27
+ To copy current feature state from Redis, see
28
+ [Migrate from Redis](rollout-active_record-adapter/README.md#migrate-from-redis).
29
+
16
30
  ## How it works
17
31
 
18
32
  Initialize a rollout object. I assign it to a global var.
19
33
 
20
34
  ```ruby
21
- require 'redis'
35
+ require "redis"
36
+ require "rollout"
37
+ require "rollout-redis-adapter"
22
38
 
23
39
  $redis = Redis.new
24
- $rollout = Rollout.new($redis)
25
- ```
26
-
27
- or even simpler
28
-
29
- ```ruby
30
- require 'redis'
31
- $rollout = Rollout.new($redis) # Will use REDIS_URL env var or default redis url
40
+ $rollout = Rollout.new(adapter: Rollout::Adapters::Redis.new($redis))
32
41
  ```
33
42
 
34
43
 
@@ -109,11 +118,12 @@ $rollout.activate_percentage(:chat, 20)
109
118
  The algorithm for determining which users get let in is this:
110
119
 
111
120
  ```ruby
112
- CRC32(user.id) < (2**32 - 1) / 100.0 * percentage
121
+ Zlib.crc32(user.id.to_s) < (2**32 - 1) / 100.0 * percentage
113
122
  ```
114
123
 
115
- So, for 20%, users 0, 1, 10, 11, 20, 21, etc would be allowed in. Those users
116
- would remain in as the percentage increases.
124
+ The result is deterministic: the same user is always in or out at a given
125
+ percentage, and users already included remain included as the percentage
126
+ increases.
117
127
 
118
128
  Deactivate all percentages like this:
119
129
 
@@ -128,7 +138,10 @@ In some cases you might want to have a feature activated for a random set of
128
138
  users. It can come specially handy when using Rollout for split tests.
129
139
 
130
140
  ```ruby
131
- $rollout = Rollout.new($redis, randomize_percentage: true)
141
+ $rollout = Rollout.new(
142
+ adapter: Rollout::Adapters::Redis.new($redis),
143
+ randomize_percentage: true,
144
+ )
132
145
  ```
133
146
 
134
147
  When on `randomize_percentage` will make sure that 50% of users for feature A
@@ -166,8 +179,9 @@ failure detection code.
166
179
  You can inspect the state of your feature using:
167
180
 
168
181
  ```ruby
169
- >> $rollout.get(:chat)
170
- => #<Rollout::Feature:0x00007f99fa4ec528 @data={}, @groups=[:caretakers], @name=:chat, @options={}, @percentage=0.05, @users=["1"]>
182
+ feature = $rollout.get(:chat)
183
+ feature.to_hash
184
+ # => { percentage: 5.0, groups: [:caretakers], users: ["1"], data: {} }
171
185
  ```
172
186
 
173
187
  ## Namespacing
@@ -180,18 +194,32 @@ environments by using the
180
194
  [redis-namespace](https://github.com/resque/redis-namespace) gem.
181
195
 
182
196
  ```ruby
197
+ gem "redis-namespace"
198
+ ```
199
+
200
+ ```ruby
201
+ require "redis"
202
+ require "redis/namespace"
203
+ require "rollout"
204
+ require "rollout-redis-adapter"
205
+
183
206
  $ns = Redis::Namespace.new(Rails.env, redis: $redis)
184
- $rollout = Rollout.new($ns)
207
+ $rollout = Rollout.new(adapter: Rollout::Adapters::Redis.new($ns))
185
208
  $rollout.activate_group(:chat, :all)
186
209
  ```
187
210
 
188
- This example would use the "development:feature:chat:groups" key.
211
+ This example stores the chat feature at `development:feature:chat` when
212
+ `Rails.env` is `"development"`.
189
213
 
190
214
  ## Frontend / UI
191
215
 
192
216
  * [rollout-ui](https://github.com/fetlife/rollout-ui)
193
217
  * [Rollout-Dashboard](https://github.com/fiverr/rollout_dashboard/)
194
218
 
219
+ These integrations may not yet support Rollout 3. Use a version compatible with
220
+ the Rollout release you install. If you depend on rollout-ui, wait for a
221
+ Rollout 3-compatible UI release before upgrading production.
222
+
195
223
  ## Implementations in other languages
196
224
 
197
225
  * Python: https://github.com/asenchi/proclaim
@@ -207,13 +235,65 @@ This example would use the "development:feature:chat:groups" key.
207
235
  * Eric Rafaloff - Maintainer - https://github.com/EricR
208
236
 
209
237
 
210
- ## Releasing
238
+ ## Testing
239
+
240
+ Install dependencies first. Core and the adapter gems have separate Gemfiles:
241
+
242
+ ```bash
243
+ bundle install
244
+ bundle install --gemfile=rollout-redis-adapter/Gemfile
245
+ bundle install --gemfile=rollout-active_record-adapter/Gemfile
246
+ ```
211
247
 
212
- 1. Bump version: `rake version:patch` (or `minor`/`major`)
213
- 2. Commit and tag: `git commit -am "Bump version" && git tag v0.7.3`
214
- 3. Push: `git push origin master --tags`
248
+ Core tests do not need Redis:
249
+
250
+ ```bash
251
+ bundle exec rake spec
252
+ ```
253
+
254
+ Redis adapter tests flush database 7 by default. Use a disposable instance,
255
+ not a shared or production Redis. Start Redis in a separate terminal:
256
+
257
+ ```bash
258
+ docker run --rm -p 6379:6379 redis:7-alpine
259
+ ```
260
+
261
+ Then run:
262
+
263
+ ```bash
264
+ bundle exec rake spec:redis
265
+ ```
266
+
267
+ Optional connection settings: `REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`. `REDIS_DB`
268
+ overrides the default database 7 that is flushed before each example.
269
+
270
+ Active Record adapter tests default to SQLite. PostgreSQL and MySQL are also
271
+ supported:
272
+
273
+ ```bash
274
+ bundle exec rake spec:active_record
275
+ ROLLOUT_AR_ADAPTER=postgresql bundle exec rake spec:active_record
276
+ ROLLOUT_AR_ADAPTER=mysql2 bundle exec rake spec:active_record
277
+ ```
278
+
279
+ ## Releasing
215
280
 
216
- The GitHub Actions workflow will automatically publish to RubyGems when tags are pushed.
281
+ Each gem has its own version and tag.
282
+
283
+ - Configure a RubyGems trusted publisher for the gem you are releasing. Use
284
+ repository owner `fetlife`, repository `rollout`, workflow filename
285
+ `release.yml`, and no GitHub environment.
286
+ - Update and commit the version in `lib/rollout/version.rb`,
287
+ `rollout-redis-adapter/rollout-redis-adapter.gemspec`, or
288
+ `rollout-active_record-adapter/rollout-active_record-adapter.gemspec`.
289
+ - Tag the release commit with `rollout/vX.Y.Z`,
290
+ `rollout-redis-adapter/vX.Y.Z`, or
291
+ `rollout-active_record-adapter/vX.Y.Z`, matching the gem version.
292
+ - Push the tag with `git push origin <tag>`. CI publishes the selected gem and
293
+ creates its GitHub release.
294
+
295
+ Use package-prefixed tags, not `vX.Y.Z`. Publish core first when the adapter
296
+ depends on a new core version.
217
297
 
218
298
  ## Copyright
219
299
 
data/Rakefile CHANGED
@@ -1,9 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
3
  require "rspec/core/rake_task"
5
4
 
6
- RSpec::Core::RakeTask.new(:spec)
5
+ RSpec::Core::RakeTask.new(:spec) do |task|
6
+ task.pattern = "spec/**/*_spec.rb"
7
+ end
8
+
9
+ namespace :spec do
10
+ desc "Run Redis adapter tests"
11
+ task :redis do
12
+ gemfile = File.expand_path("rollout-redis-adapter/Gemfile", __dir__)
13
+ Dir.chdir("rollout-redis-adapter") do
14
+ Bundler.with_unbundled_env do
15
+ sh({ "BUNDLE_GEMFILE" => gemfile }, "bundle exec rspec")
16
+ end
17
+ end
18
+ end
19
+
20
+ desc "Run Active Record adapter tests"
21
+ task :active_record do
22
+ gemfile = File.expand_path("rollout-active_record-adapter/Gemfile", __dir__)
23
+ Dir.chdir("rollout-active_record-adapter") do
24
+ Bundler.with_unbundled_env do
25
+ sh({ "BUNDLE_GEMFILE" => gemfile }, "bundle exec rspec")
26
+ end
27
+ end
28
+ end
29
+ end
7
30
 
8
31
  task default: :spec
9
32
 
@@ -0,0 +1,110 @@
1
+ # Upgrading to Rollout 3
2
+
3
+ Rollout 3 keeps feature evaluation in the `rollout` gem and moves Redis
4
+ persistence to `rollout-redis-adapter`. Existing Redis keys stay in place. This is
5
+ not a storage migration onto Active Record. For the Active Record adapter
6
+ and Redis cutover, see
7
+ [rollout-active_record-adapter/README.md](../rollout-active_record-adapter/README.md).
8
+
9
+ ## 1. Update dependencies
10
+
11
+ Add the adapter gem next to `rollout`:
12
+
13
+ ```ruby
14
+ gem "rollout", "~> 3.1"
15
+ gem "rollout-redis-adapter", "~> 0.1"
16
+ ```
17
+
18
+ Use compatible releases of both gems. The Redis adapter is versioned
19
+ independently of core. Rollout `3.0.0` used `backend:` and
20
+ `Rollout::Redis::Backend`; `3.1.0` uses `adapter:` and
21
+ `Rollout::Adapters::Redis`. If you use
22
+ [rollout-ui](https://github.com/fetlife/rollout-ui), wait for a Rollout
23
+ 3-compatible UI release before upgrading production.
24
+
25
+ ## 2. Update requires and initialization
26
+
27
+ ```ruby
28
+ # Before
29
+ require "redis"
30
+ require "rollout"
31
+
32
+ $redis = Redis.new
33
+ $rollout = Rollout.new(
34
+ $redis,
35
+ randomize_percentage: true,
36
+ logging: { history_length: 100, global: true },
37
+ )
38
+ ```
39
+
40
+ ```ruby
41
+ # After
42
+ require "redis"
43
+ require "rollout"
44
+ require "rollout-redis-adapter"
45
+
46
+ $redis = Redis.new
47
+ $rollout = Rollout.new(
48
+ adapter: Rollout::Adapters::Redis.new($redis),
49
+ randomize_percentage: true,
50
+ logging: { history_length: 100, global: true },
51
+ )
52
+ ```
53
+
54
+ Pass the same Redis client, database, and namespace you already use. Repeat
55
+ this change at every initialization site, including jobs, scripts, and
56
+ consoles.
57
+
58
+ `redis-namespace` still works:
59
+
60
+ ```ruby
61
+ $ns = Redis::Namespace.new(Rails.env, redis: $redis)
62
+ $rollout = Rollout.new(adapter: Rollout::Adapters::Redis.new($ns))
63
+ ```
64
+
65
+ ## 3. Breaking changes
66
+
67
+ | Area | Breaking change | Required action |
68
+ | --- | --- | --- |
69
+ | Storage configuration | `Rollout.new(redis, options)` now requires `adapter:`. | Wrap the existing client in `Rollout::Adapters::Redis.new(redis)` and pass options as keywords. |
70
+ | Storage access | `rollout.storage` is removed. `rollout.adapter` returns an adapter, not the Redis client. | Keep your own Redis client reference if application code needs direct access. |
71
+ | Logging storage | `logging: { storage: other_redis }` is no longer supported. The Redis backend stores features and history through the same client. | Applications using separate history storage cannot preserve that setup with the current adapter. Removing the option does not migrate existing history. |
72
+ | Custom logging | `Logger#log` and `Logger#update` are removed. Built-in logging is no longer an observer. | Use Rollout mutations and `logging.with_context` rather than calling those methods directly. |
73
+ | Feature construction | `Feature.new(name, rollout:, state: payload)` no longer accepts a name argument or raw Redis payload. | Prefer `rollout.get(name)`. Direct construction requires `Feature.new(state: feature_state, rollout: rollout, options: rollout.options)`. |
74
+ | Feature serialization | `feature.serialize` is removed. | Use `feature.to_feature_state` for a backend-neutral snapshot. Redis encoding belongs to `Rollout::Redis::Codec`. |
75
+ | Feature metadata | Metadata is canonicalized to JSON. Symbol keys and values become strings. Values such as `Time`, `Date`, and `BigDecimal` persist as their JSON representations, not as original Ruby objects. | No data migration is required. JSON-serializable writes continue to work. |
76
+ | History decoding | `Logging::Event.from_raw` is removed. | Decode persisted Redis members with `Rollout::Redis::Codec.decode_event(value, score)`. |
77
+
78
+ ## 4. History and lifecycle
79
+
80
+ History reads can request a bound. `limit` is the newest N events, returned
81
+ oldest-to-newest. Omit `limit` to read the full retained history. `0` returns
82
+ no events.
83
+
84
+ ```ruby
85
+ rollout.logging.events(:chat, limit: 10)
86
+ rollout.logging.global_events(limit: 10)
87
+ rollout.logging.last_event(:chat)
88
+ ```
89
+
90
+ `last_event` reads one persisted member. It does not load the complete
91
+ history.
92
+
93
+ | Operation | Feature state | Per-feature history | Global history |
94
+ | --- | --- | --- | --- |
95
+ | `delete`, logging enabled | Removed | Removed | Retained; emits a global `delete` event when `global: true` and the adapter supports event-aware deletion |
96
+ | `delete`, logging disabled | Removed | Retained | Retained |
97
+ | `logging.delete` | Unchanged | Removed | Retained |
98
+ | `clear!` | Removed | Retained | Retained |
99
+
100
+ The deletion event includes the feature name, timestamp, and current logging
101
+ context (for example, an actor supplied with `logging.with_context`). Built-in
102
+ adapters write it only when the feature exists and global logging is enabled;
103
+ it is subject to `history_length` like other global events. Older or custom
104
+ adapters without `delete_feature_with_history` retain the previous deletion
105
+ behavior and do not record a deletion event.
106
+
107
+ `clear!` still resets each feature first, so logging-enabled instances record
108
+ a reset event before the state is deleted. History remains subject to
109
+ `history_length`. The Redis registry key is removed after clearing, including
110
+ when it was already empty.