redis-objects-periodical 0.7.0 → 0.8.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/skills/dependabot-batch-maintenance/SKILL.md +141 -0
  3. data/.github/dependabot.yml +32 -3
  4. data/.github/scripts/dependabot-auto-merge.cjs +182 -0
  5. data/.github/scripts/dependabot-auto-merge.test.cjs +278 -0
  6. data/.github/scripts/dependabot-policy.cjs +132 -0
  7. data/.github/scripts/dependabot-policy.test.cjs +235 -0
  8. data/.github/scripts/timezone-inventory.cjs +48 -0
  9. data/.github/scripts/timezone-inventory.test.cjs +86 -0
  10. data/.github/workflows/checks.yml +124 -0
  11. data/.github/workflows/ci.yml +191 -0
  12. data/.github/workflows/dependabot-auto-merge.yml +61 -0
  13. data/.github/workflows/release.yml +59 -0
  14. data/.github/workflows/rubocop_challenge.yml +71 -0
  15. data/.rspec +1 -0
  16. data/.rubocop.yml +1 -1
  17. data/.rubocop_todo.yml +16 -16
  18. data/AGENTS.md +133 -0
  19. data/CHANGELOG.md +47 -0
  20. data/CONTRIBUTING.md +203 -0
  21. data/Dockerfile +3 -2
  22. data/Gemfile +6 -6
  23. data/Gemfile.lock +70 -65
  24. data/README.md +58 -221
  25. data/docker-compose.yml +1 -3
  26. data/docs/maintaining.md +237 -0
  27. data/docs/usage.md +216 -0
  28. data/docs/verification/2026-10-27-timezone.md +25 -0
  29. data/gemfiles/active_support/Gemfile +5 -0
  30. data/gemfiles/active_support/Gemfile.lock +137 -0
  31. data/lib/redis/objects/periodical/version.rb +1 -1
  32. data/lib/redis/objects/periodical_counters.rb +1 -1
  33. data/lib/redis/objects/periodical_hashes.rb +1 -1
  34. data/lib/redis/objects/periodical_sets.rb +1 -1
  35. data/lib/redis/objects/periodical_values.rb +1 -1
  36. data/lib/redis/periodical_counter.rb +1 -1
  37. data/lib/redis/periodical_hash_key.rb +1 -1
  38. data/lib/redis/periodical_set.rb +1 -1
  39. data/lib/redis/periodical_value.rb +1 -1
  40. data/lib/redis/recurring_at_intervals/weekly.rb +3 -1
  41. data/lib/redis/recurring_at_intervals.rb +3 -3
  42. data/redis-objects-periodical.gemspec +1 -1
  43. metadata +26 -8
  44. data/.circleci/config.yml +0 -212
data/README.md CHANGED
@@ -1,263 +1,100 @@
1
- [![CircleCI](https://circleci.com/gh/ryz310/redis-objects-periodical.svg?style=svg)](https://circleci.com/gh/ryz310/redis-objects-periodical) [![Gem Version](https://badge.fury.io/rb/redis-objects-periodical.svg)](https://badge.fury.io/rb/redis-objects-periodical) [![Maintainability](https://api.codeclimate.com/v1/badges/deebb6c406c306a6f337/maintainability)](https://codeclimate.com/github/ryz310/redis-objects-periodical/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/deebb6c406c306a6f337/test_coverage)](https://codeclimate.com/github/ryz310/redis-objects-periodical/test_coverage)
1
+ [![CI](https://github.com/ryz310/redis-objects-periodical/actions/workflows/ci.yml/badge.svg)](https://github.com/ryz310/redis-objects-periodical/actions/workflows/ci.yml) [![Gem Version](https://badge.fury.io/rb/redis-objects-periodical.svg)](https://badge.fury.io/rb/redis-objects-periodical)
2
+ [![Maintainability](https://qlty.sh/gh/ryz310/projects/redis-objects-periodical/maintainability.svg)](https://qlty.sh/gh/ryz310/projects/redis-objects-periodical)
3
+ [![Code Coverage](https://qlty.sh/gh/ryz310/projects/redis-objects-periodical/coverage.svg)](https://qlty.sh/gh/ryz310/projects/redis-objects-periodical)
2
4
 
3
5
  # Redis::Objects::Periodical
4
6
 
5
- This is a gem which extends [Redis::Objects](https://github.com/nateware/redis-objects) gem. Once install this gem, you can use the periodical counter, the periodical set, etc. in addition to the standard features of Redis::Objects. These counters and sets are useful for measuring conversions, implementing API rate limiting, MAU, DAU, and more.
7
+ Extend [Redis::Objects](https://github.com/nateware/redis-objects) with counters,
8
+ values, sets, and hashes whose Redis keys are partitioned by period. Use them for
9
+ page views, active-user counts, rate-limiting data, and other periodical values.
6
10
 
7
11
  ## Installation
8
12
 
13
+ Ruby 3.3 or newer is required. Ruby 3.3, 3.4, and 4.0 are tested in CI.
14
+ The support policy follows stable Ruby branches receiving official normal or
15
+ security maintenance; preview releases are not supported. CI uses the latest
16
+ patch releases available in each supported series' CircleCI Ruby image.
17
+ See [Ruby's maintenance status](https://www.ruby-lang.org/en/downloads/branches/).
18
+
19
+ Ruby 2.7, 3.0, 3.1, and 3.2 are no longer supported. Applications using those
20
+ versions must upgrade Ruby before upgrading this gem, or remain on the last gem
21
+ release whose `required_ruby_version` permits their Ruby version.
22
+
9
23
  Add this line to your application's Gemfile:
10
24
 
11
25
  ```ruby
12
26
  gem 'redis-objects-periodical'
13
27
  ```
14
28
 
15
- If you want to know about installation and standard usage, please see Redis::Objects' GitHub page.
29
+ ## Quick start
16
30
 
17
- ## Usage
31
+ Configure the Redis connection as described in
32
+ [Redis::Objects](https://github.com/nateware/redis-objects), then declare a field:
18
33
 
19
- `daily_counter` and `daily_set` automatically creates keys that are unique to each object, in the format:
20
-
21
- ```
22
- model_name:id:field_name:yyyy-mm-dd
23
- ```
24
-
25
- I recommend using with `expireat` option.
26
- For illustration purposes, consider this stub class:
34
+ ```ruby
35
+ require 'redis-objects-periodical'
27
36
 
28
- ```rb
29
37
  class Homepage
30
38
  include Redis::Objects
31
39
 
32
40
  daily_counter :pv, expireat: -> { Time.now + 2_678_400 } # about a month
33
- daily_hash_key :browsing_history, expireat: -> { Time.now + 2_678_400 } # about a month
34
- daily_set :dau, expireat: -> { Time.now + 2_678_400 } # about a month
35
- daily_value :cache, expireat: -> { Time.now + 2_678_400 } # about a month
36
41
 
37
42
  def id
38
43
  1
39
44
  end
40
45
  end
41
46
 
42
- # 2021-04-01
43
47
  homepage = Homepage.new
44
- homepage.id # 1
45
-
46
- homepage.pv.increment
47
- homepage.pv.increment
48
- homepage.pv.increment
49
- puts homepage.pv.value # 3
50
-
51
- # 2021-04-02 (next day)
52
- puts homepage.pv.value # 0
53
- homepage.pv.increment
54
- homepage.pv.increment
55
- puts homepage.pv.value # 2
56
-
57
- start_date = Date.new(2021, 4, 1)
58
- end_date = Date.new(2021, 4, 2)
59
- homepage.pv.range(start_date, end_date) # [3, 2]
60
- ```
61
-
62
- ### Periodical Counters
63
-
64
- The periodical counters automatically switches the save destination when the date changes.
65
- You can access past dates counted values like Ruby arrays:
66
-
67
- ```rb
68
- # 2021-04-01
48
+ # On 2021-04-01:
69
49
  homepage.pv.increment(3)
50
+ homepage.pv.value # => 3
70
51
 
71
- # 2021-04-02 (next day)
52
+ # On 2021-04-02, calling the field accessor selects the new day's key:
53
+ homepage.pv.value # => 0
72
54
  homepage.pv.increment(2)
73
-
74
- # 2021-04-03 (next day)
75
- homepage.pv.increment(5)
76
-
77
- homepage.pv[Date.new(2021, 4, 1)] # => 3
78
- homepage.pv[Date.new(2021, 4, 1), 3] # => [3, 2, 5]
79
- homepage.pv[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => [3, 2]
80
-
81
- homepage.pv.delete_at(Date.new(2021, 4, 1))
82
- homepage.pv.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [0, 2, 5]
83
- homepage.pv.at(Date.new(2021, 4, 2)) # => #<Redis::Counter key="homepage:1:pv:2021-04-02">
84
- homepage.pv.at(Date.new(2021, 4, 2)).value # 2
55
+ homepage.pv.range(Date.new(2021, 4, 1), Date.new(2021, 4, 2)) # => [3, 2]
85
56
  ```
86
57
 
87
- #### Periodical Counters Family
88
-
89
- - `annual_counter`
90
- - Key format: `model_name:id:field_name:yyyy`
91
- - Redis is a highly volatile key-value store, so I don't recommend using it.
92
- - `monthly_counter`
93
- - Key format: `model_name:id:field_name:yyyy-mm`
94
- - `weekly_counter`
95
- - Key format: `model_name:id:field_name:yyyyWw`
96
- - `daily_counter`
97
- - Key format: `model_name:id:field_name:yyyy-mm-dd`
98
- - `hourly_counter`
99
- - Key format: `model_name:id:field_name:yyyy-mm-ddThh`
100
- - `minutely_counter`
101
- - Key format: `model_name:id:field_name:yyyy-mm-ddThh:mi`
102
-
103
- ### Periodical Values
104
-
105
- The periodical values automatically switches the save destination when the date changes.
106
-
107
- ```rb
108
- # 2021-04-01
109
- homepage.cache.value = 'a'
110
-
111
- # 2021-04-02 (next day)
112
- homepage.cache.value = 'b'
113
-
114
- # 2021-04-03 (next day)
115
- homepage.cache.value = 'c'
116
-
117
- homepage.cache[Date.new(2021, 4, 1)] # => 'a'
118
- homepage.cache[Date.new(2021, 4, 1), 3] # => ['a', 'b', 'c']
119
- homepage.cache[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => ['a', 'b']
120
-
121
- homepage.cache.delete_at(Date.new(2021, 4, 1))
122
- homepage.cache.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [nil, 'b', 'c']
123
- homepage.cache.at(Date.new(2021, 4, 2)) # => #<Redis::Value key="homepage:1:cache:2021-04-02">
124
- homepage.cache.at(Date.new(2021, 4, 2)).value # 'b'
125
- ```
58
+ Keys are unique to the model, its ID, field, and period. This daily counter uses
59
+ `homepage:1:pv:2021-04-01`. Each field accessor creates a new periodical object;
60
+ an object retained in a variable keeps its original key across period boundaries.
61
+ Use an appropriate expiration option to limit retained period keys.
126
62
 
127
- #### Periodical Values Family
63
+ ## Available periods and types
128
64
 
129
- - `annual_value`
130
- - Key format: `model_name:id:field_name:yyyy`
131
- - Redis is a highly volatile key-value store, so I don't recommend using it.
132
- - `monthly_value`
133
- - Key format: `model_name:id:field_name:yyyy-mm`
134
- - `weekly_value`
135
- - Key format: `model_name:id:field_name:yyyyWw`
136
- - `daily_value`
137
- - Key format: `model_name:id:field_name:yyyy-mm-dd`
138
- - `hourly_value`
139
- - Key format: `model_name:id:field_name:yyyy-mm-ddThh`
140
- - `minutely_value`
141
- - Key format: `model_name:id:field_name:yyyy-mm-ddThh:mi`
65
+ Combine one of `annual`, `monthly`, `weekly`, `daily`, `hourly`, or `minutely`
66
+ with one of these field types:
142
67
 
143
- ### Periodical Hashes
68
+ | Type | Daily declaration | Single-period result | Multi-period result |
69
+ | --- | --- | --- | --- |
70
+ | Counter | `daily_counter` | Integer, missing as `0` | Array of integers |
71
+ | Value | `daily_value` | Stored value, missing as `nil` | Array of values |
72
+ | Set | `daily_set` | Members, missing as `[]` | Union of members |
73
+ | Hash | `daily_hash_key` | Hash, missing as `{}` | Hash with combined field values |
144
74
 
145
- The periodical hashes also automatically switches the save destination when the date changes.
75
+ See the [usage guide](docs/usage.md) for declarations, key suffixes, weekly
76
+ year-boundary behavior, date/time arguments, aggregation, expiration, and timezone
77
+ selection. Rails/ActiveSupport is optional.
146
78
 
147
- ```rb
148
- # 2021-04-01
149
- homepage.browsing_history.incr('item1')
150
- homepage.browsing_history.incr('item2')
151
- homepage.browsing_history.incr('item2')
152
- puts homepage.browsing_history.all # { 'item1' => '1', 'item2' => '2' }
79
+ ## Development and maintenance
153
80
 
154
- # 2021-04-02 (next day)
155
- puts homepage.browsing_history.all # {}
156
-
157
- homepage.browsing_history.bulk_set('item1' => 3, 'item3' => 5)
158
- puts homepage.browsing_history.all # { 'item1' => '3', 'item3' => '5' }
159
-
160
- # 2021-04-03 (next day)
161
- homepage.browsing_history.incr('item2')
162
- homepage.browsing_history.incr('item4')
163
- puts homepage.browsing_history.all # { 'item2' => '1', 'item4' => '1' }
164
-
165
- homepage.browsing_history[Date.new(2021, 4, 1)] # => { 'item1' => '1', 'item2' => '2' }
166
- homepage.browsing_history[Date.new(2021, 4, 1), 3] # => { 'item1' => '4', 'item2' => '3', 'item3' => '5', 'item4' => '1' }
167
- homepage.browsing_history[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => { 'item1' => '4', 'item2' => '2', 'item3' => '5' }
168
-
169
- homepage.browsing_history.delete_at(Date.new(2021, 4, 1))
170
- homepage.browsing_history.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => { 'item1' => '3', 'item2' => '1', 'item3' => '5', 'item4' => '1' }
171
- homepage.browsing_history.at(Date.new(2021, 4, 2)) # => #<Redis::HashKey key="homepage:1:browsing_history:2021-04-02">
172
- homepage.browsing_history.at(Date.new(2021, 4, 2)).all # { 'item1' => '3', 'item3' => '5' }
173
- ```
174
-
175
- #### Periodical Hashes Family
176
-
177
- - `annual_hash_key`
178
- - Key format: `model_name:id:field_name:yyyy`
179
- - Redis is a highly volatile key-value store, so I don't recommend using it.
180
- - `monthly_hash_key`
181
- - Key format: `model_name:id:field_name:yyyy-mm`
182
- - `weekly_hash_key`
183
- - Key format: `model_name:id:field_name:yyyyWw`
184
- - `daily_hash_key`
185
- - Key format: `model_name:id:field_name:yyyy-mm-dd`
186
- - `hourly_hash_key`
187
- - Key format: `model_name:id:field_name:yyyy-mm-ddThh`
188
- - `minutely_hash_key`
189
- - Key format: `model_name:id:field_name:yyyy-mm-ddThh:mi`
190
-
191
- ### Periodical Sets
192
-
193
- The periodical sets also automatically switches the save destination when the date changes.
194
-
195
- ```rb
196
- # 2021-04-01
197
- homepage.dau << 'user1'
198
- homepage.dau << 'user2'
199
- homepage.dau << 'user1' # dup ignored
200
- puts homepage.dau.members # ['user1', 'user2']
201
- puts homepage.dau.length # 2
202
- puts homepage.dau.count # alias of #length
203
-
204
- # 2021-04-02 (next day)
205
- puts homepage.dau.members # []
206
-
207
- homepage.dau.merge('user2', 'user3')
208
- puts homepage.dau.members # ['user2', 'user3']
209
-
210
- # 2021-04-03 (next day)
211
- homepage.dau.merge('user4')
212
-
213
- homepage.dau[Date.new(2021, 4, 1)] # => ['user1', 'user2']
214
- homepage.dau[Date.new(2021, 4, 1), 3] # => ['user1', 'user2', 'user3', 'user4']
215
- homepage.dau[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => ['user1', 'user2', 'user3']
216
-
217
- homepage.dau.delete_at(Date.new(2021, 4, 1))
218
- homepage.dau.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => ['user2', 'user3', 'user4']
219
- homepage.dau.at(Date.new(2021, 4, 2)) # => #<Redis::Set key="homepage:1:dau:2021-04-02">
220
- homepage.dau.at(Date.new(2021, 4, 2)).members # ['user2', 'user3']
221
- ```
222
-
223
- #### Periodical Sets Family
224
-
225
- - `annual_set`
226
- - Key format: `model_name:id:field_name:yyyy`
227
- - Redis is a highly volatile key-value store, so I don't recommend using it.
228
- - `monthly_set`
229
- - Key format: `model_name:id:field_name:yyyy-mm`
230
- - `weekly_set`
231
- - Key format: `model_name:id:field_name:yyyyWw`
232
- - `daily_set`
233
- - Key format: `model_name:id:field_name:yyyy-mm-dd`
234
- - `hourly_set`
235
- - Key format: `model_name:id:field_name:yyyy-mm-ddThh`
236
- - `minutely_set`
237
- - Key format: `model_name:id:field_name:yyyy-mm-ddThh:mi`
238
-
239
- ### Timezone
240
-
241
- This gem follows Ruby process' time zone, but if you extends Time class by ActiveSupport (e.g. `Time.current`), follows Rails process' timezone.
242
-
243
- ## Development
244
-
245
- The development environment for this gem is configured with docker-compose.
246
- Please use the following command:
247
-
248
- $ docker-compose up -d
249
- $ docker-compose run --rm ruby bundle
250
- $ docker-compose run --rm ruby rspec .
251
- $ docker-compose run --rm ruby rubocop -a
81
+ - [Contributing](CONTRIBUTING.md): Docker setup, isolated Compose projects,
82
+ specs, lint, builds, CI, and optional ActiveSupport verification.
83
+ - [Maintaining](docs/maintaining.md): dependency updates, auto-merge, coverage
84
+ configuration, releases, scheduled maintenance, and CircleCI cutover/rollback.
85
+ - [Agent guidance](AGENTS.md): repository workflow and compatibility constraints.
86
+ - [Changelog](CHANGELOG.md): released changes.
252
87
 
253
88
  ## Contributing
254
89
 
255
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/redis-objects-periodical. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/redis-objects-periodical/blob/master/CODE_OF_CONDUCT.md).
256
-
257
- ## License
258
-
259
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
90
+ Bug reports and pull requests are welcome on
91
+ [GitHub](https://github.com/ryz310/redis-objects-periodical).
92
+ Track unresolved bugs, improvements, and decisions in
93
+ [GitHub Issues](https://github.com/ryz310/redis-objects-periodical/issues).
94
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
260
95
 
261
- ## Code of Conduct
96
+ ## License and code of conduct
262
97
 
263
- Everyone interacting in the Redis::Objects::Daily::Counter project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/redis-objects-periodical/blob/master/CODE_OF_CONDUCT.md).
98
+ The gem is available under the [MIT License](LICENSE.txt).
99
+ Everyone interacting with the Redis::Objects::Periodical project is expected to
100
+ follow the [code of conduct](CODE_OF_CONDUCT.md).
data/docker-compose.yml CHANGED
@@ -9,9 +9,7 @@ services:
9
9
  - .:/my_app
10
10
  - bundle:/usr/local/bundle
11
11
  redis:
12
- image: redis:latest
13
- ports:
14
- - 6379:6379
12
+ image: redis:latest@sha256:97ec88159e6d7cfb5ddef9015386087d58a912de80112f06ebdfa78fa5989901
15
13
 
16
14
  volumes:
17
15
  bundle:
@@ -0,0 +1,237 @@
1
+ # Maintaining
2
+
3
+ See [Contributing](../CONTRIBUTING.md) for development and test commands.
4
+ Keep unresolved work in [GitHub Issues](https://github.com/ryz310/redis-objects-periodical/issues).
5
+ This guide describes procedures; it does not certify that external settings or
6
+ CircleCI cutover have been completed.
7
+
8
+ ## Coverage reporting
9
+
10
+ To enable coverage reporting to Qlty, enable this repository in Qlty Cloud and
11
+ register a dedicated project coverage token as the GitHub Actions repository
12
+ secret `QLTY_COVERAGE_TOKEN`. Keep secret variables unavailable to fork and
13
+ Dependabot pull requests. When the token is absent, CI still merges and stores
14
+ coverage but explicitly skips the upload. When present, the SHA-pinned Qlty
15
+ Action publishes the merged report with `skip-errors: false`; upload errors fail
16
+ that job. Never commit the token or print its value in logs.
17
+
18
+ Qlty maintainability analysis and quality gates are configured separately in
19
+ Qlty Cloud. Review their results before making new checks required. Coverage
20
+ upload alone does not enable or verify those checks.
21
+
22
+ ## Dependency updates and auto-merge
23
+
24
+ Dependabot checks Bundler and GitHub Actions weekly on Monday at 08:00 JST.
25
+ Bundler version updates wait 3 days for patches, 7 for minor updates, and 14 for
26
+ major updates (7 days by default). Actions updates use a 7-day default cooldown;
27
+ SemVer-specific cooldown settings are not applied to that ecosystem. These
28
+ cooldowns apply to version updates, not security updates.
29
+
30
+ Bundler updates cover both `/` and `/gemfiles/active_support`. The optional
31
+ integration uses a standard `Gemfile`/`Gemfile.lock` pair so Dependabot can discover
32
+ it. Its Gemfile evaluates the root Gemfile; review shared dependency updates in
33
+ both lockfiles. The `shared-dependencies` rule uses `group-by: dependency-name` to
34
+ combine each dependency's version update across the two directories into one PR,
35
+ while keeping different dependencies in separate PRs. See the
36
+ [Dependabot grouping reference](https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference#group-by-groups).
37
+ Cross-directory grouping applies to version updates; review security updates for
38
+ the same lockfile consistency separately. Keep frozen installs enabled in CI.
39
+
40
+ Updates that touch the optional lockfile require manual review under the existing
41
+ auto-merge policy, including shared development dependency updates. ActiveSupport
42
+ remains optional, and changes to its selected series follow the
43
+ [version selection policy](../CONTRIBUTING.md#activesupport-version-selection).
44
+ For a manual shared dependency update, update the root constraint/lockfile and
45
+ resolve the optional lockfile with that dependency as the target, for example:
46
+
47
+ ```sh
48
+ docker compose run --rm -e BUNDLE_GEMFILE=gemfiles/active_support/Gemfile ruby bundle lock --update simplecov --conservative
49
+ ```
50
+
51
+ When changing the monitored Bundler directories, let the configuration land on
52
+ `master` before using `@dependabot recreate` on PRs generated under the old
53
+ configuration. Verify that each shared dependency update includes both lockfiles
54
+ and passes the Ruby matrix, timezone inventory, and coverage collation. Recreation
55
+ overwrites manual changes; if Dependabot replaces the old PR with a grouped PR,
56
+ review the replacement. A successful configuration PR alone does not establish
57
+ that the hosted Dependabot update has synchronized both lockfiles.
58
+
59
+ `dependabot-auto-merge.yml` can reserve auto-merge for a narrow set of updates:
60
+
61
+ - Bundler patch/minor updates of `pry-byebug`, `rake`, `rspec`,
62
+ `rspec_junit_formatter`, `rubocop`, `rubocop-performance`, `rubocop-rspec`,
63
+ `simplecov`, `timecop`, and `yard`, changing only `Gemfile.lock`. The locked
64
+ runtime dependency entries (`redis-objects-periodical`, `redis-objects`,
65
+ `redis`, `redis-client`, and `connection_pool`) must stay identical. Only gem
66
+ spec/version and transitive dependency requirement lines may change.
67
+ - SHA-pinned patch/minor replacements for `actions/checkout`,
68
+ `actions/upload-artifact`, `actions/download-artifact`, and
69
+ `actions/github-script`, changing only `uses:` lines and their version comments
70
+ in existing workflow files.
71
+
72
+ Every named dependency in a grouped update must be permitted. Major updates,
73
+ Bundler itself, runtime dependencies, optional integration lockfiles, manifest
74
+ constraints, source/platform changes, third-party Actions, and workflow
75
+ permission/trigger/step changes require manual review. Unknown metadata, missing
76
+ or incomplete patches, a changed PR head, forks, and API errors do not enable
77
+ merging. The enable mutation also requires the exact inspected head commit.
78
+ If GitHub rejects enabling auto-merge specifically because the PR is already in
79
+ clean status, the workflow repeats policy and protection verification and checks
80
+ the current head, base, open/clean state, and absence of a reservation before
81
+ merging with an exact-head guard. Other enable errors fail the workflow.
82
+
83
+ The privileged workflow uses `pull_request_target` only to obtain the write
84
+ permissions unavailable on normal Dependabot PR events. It checks out only the
85
+ trusted base commit's `.github/scripts` policy with credentials disabled, never
86
+ executes PR code, and inspects changes through the GitHub API. Its trigger has a
87
+ scoped zizmor exception for this metadata-only design. Policy regressions run in
88
+ ordinary read-only CI:
89
+
90
+ ```sh
91
+ node --test .github/scripts/*.test.cjs
92
+ ```
93
+
94
+ Repository administrators must finish these settings after the new checks have
95
+ appeared on a PR:
96
+
97
+ 1. Enable repository auto-merge with merge commits and preserve existing required
98
+ reviews, security checks, and other protections.
99
+ 2. On `master`, require status checks and require the branch to be up to date.
100
+ Keep all three `checks / build_on_ruby_*` checks, `checks / rubocop`, `yardoc`,
101
+ and `upload-coverage`; add `active-support-timezone` and `dependabot-policy`.
102
+ 3. Verify on an eligible Dependabot PR that a failing required check prevents
103
+ merging and that an excluded update stays manual. For existing PRs, let their
104
+ next update trigger the workflow.
105
+
106
+ Matching Dependabot PR events run the workflow without a separate enable
107
+ variable. The policy below verifies the repository settings before allowing a
108
+ merge.
109
+
110
+ The policy verifies the repository's auto-merge setting and the effective
111
+ rulesets plus any classic branch protection, including all eight named checks and
112
+ strict up-to-date checks, before reserving a merge. Preserve GitGuardian and all
113
+ other existing required checks. If protection cannot be read or is missing, it
114
+ rejects the update. On a current-head rejection or verification error (including
115
+ metadata-fetch failure), the workflow revokes an earlier reservation only when
116
+ its ownership can be verified. The reservation's merge commit body contains a
117
+ small policy marker identifying the repository, PR, inspected head, and workflow
118
+ run. Ownership requires both the GitHub Actions bot and a matching originating
119
+ workflow run; this marker remains in the eventual merge commit body. Human,
120
+ unrelated automation, and legacy unmarked reservations are preserved with a
121
+ manual-inspection notice. Failure to read ownership or confirm cancellation
122
+ fails the workflow and requires manual inspection of the PR's auto-merge state.
123
+
124
+ Runs for one PR are serialized, stale events do not change the current head's
125
+ reservation, and cancellation rechecks the head and reservation immediately
126
+ before writing and verifies the result afterward. GitHub's disable mutation has
127
+ no conditional head/reservation argument, so administrators and other automation
128
+ must avoid changing reservations concurrently with this workflow. A concurrent
129
+ external write between the final read and cancellation cannot be guarded
130
+ atomically by this API.
131
+
132
+ Merge queues are not supported. No branch-protection, repository-variable, or
133
+ publication setting is changed by the workflow. When rolling back,
134
+ [disable the Dependabot Auto Merge workflow](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/disable-and-enable-workflows)
135
+ to prevent new runs, cancel any active runs, and disable auto-merge on
136
+ already-reserved PRs separately. Disabling the workflow does not cancel active
137
+ runs or revoke existing reservations. Also inspect reservations manually if the
138
+ job is skipped (for example after retargeting away from `master`), checkout fails,
139
+ a run is cancelled or times out, or an
140
+ ownership marker's originating run is no longer available. Only the configured
141
+ PR events trigger re-evaluation; settings changes do not trigger it.
142
+
143
+ ## Release and scheduled maintenance
144
+
145
+ `release.yml` uses RubyGems Trusted Publishing. Only pushes or manual dispatches
146
+ on `release` can publish; no repository enable variable is required. The workflow
147
+ reruns all three Ruby checks and RuboCop against the triggering commit, then
148
+ publishes that same commit using Ruby 3.3 and Bundler 4.0.23. YARD and Qlty do not
149
+ gate publishing. The publishing job uses fresh dependencies without caches,
150
+ checks for an existing version tag or published version, and is not cancelled by
151
+ a newer run. Do not run a real release as a migration test.
152
+
153
+ Before the first publish, register this gem's Trusted Publisher on RubyGems.org
154
+ with owner `ryz310`, repository `redis-objects-periodical`, workflow filename
155
+ `release.yml`, and environment `release`. Create the matching GitHub environment,
156
+ restrict it to the `release` branch, and configure its protection rules. The
157
+ publishing job alone receives `contents: write` and `id-token: write`; it does not
158
+ use `RUBYGEMS_API_KEY`. See the [RubyGems Trusted Publishing guide](https://guides.rubygems.org/trusted-publishing/).
159
+
160
+ `rubocop_challenge.yml` runs daily at 08:30 JST on `master` without a repository
161
+ enable variable. Manual dispatch on `master` defaults to a read-only dry run using
162
+ `--no-create-pr`. The CLI requires a token even for a dry run, so that job uses
163
+ the automatic read-only `GITHUB_TOKEN`.
164
+ Actual PR creation uses `GITHUB_TOKEN` with job-scoped `contents: write` and
165
+ `pull-requests: write`. Enable **Allow GitHub Actions to create and approve pull
166
+ requests** in the repository's Actions settings before PR creation. Check the
167
+ created PR's CI and approve pending workflow runs if required; PR creation alone
168
+ does not prove CI ran. Scheduled workflows run on the default branch, may be
169
+ delayed, and can be disabled after 60 days without public repository activity.
170
+
171
+ ### Resume a release after removing the enable variables
172
+
173
+ 1. Stop and drain the old CircleCI publisher, and confirm the Trusted Publisher
174
+ and GitHub `release` environment described above are configured. Inspect the
175
+ intended version's Git tag and RubyGems version before continuing. If either
176
+ already exists, investigate the partial release instead of retrying blindly.
177
+ 2. Merge the workflow fix into `master` after its PR checks pass. This restores
178
+ the Challenge schedule on `master`; it does not update the release workflow
179
+ on the `release` branch.
180
+ 3. Open and review a PR from `master` to `release`. Confirm its complete diff
181
+ contains only the changes intended for this release. Merging this PR pushes
182
+ to `release` and starts publication automatically, so merge only when ready
183
+ to publish. No enable variable or additional manual dispatch is needed.
184
+ 4. Follow the new Release run. It must pass the three Ruby builds and RuboCop
185
+ before `publish` starts. Approve the `release` environment deployment if its
186
+ configured protection rules require approval. Confirm the version tag and
187
+ published RubyGems version after the job succeeds.
188
+
189
+ If the fixed workflow is already on `release` and no publish is in progress,
190
+ manual dispatch of **Release** with branch **release** starts a new release run.
191
+ Do not re-run the old skipped run: [a re-run uses its original commit](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs)
192
+ and still contains the removed conditions. Dispatching on `master` cannot publish.
193
+
194
+ ## CircleCI migration checklist
195
+
196
+ The CircleCI configuration and badge have been removed. Repository administrators
197
+ must complete the external settings below; removing a file does not update branch
198
+ protection, stop already queued jobs, or disable workflows still present on the
199
+ old `release` branch. Stop each CircleCI writer before activating the matching
200
+ Actions workflow. The Challenge schedule becomes active when its workflow is on
201
+ `master`; publication starts on a push or manual dispatch on `release`.
202
+
203
+ 1. Verify Actions on a normal PR, a fork PR, and a Dependabot PR. Check all three
204
+ Ruby builds, RuboCop, YARD, JUnit artifacts, and the three-input coverage merge.
205
+ Confirm missing-token skipping and authenticated Qlty reporting separately.
206
+ 2. Once the Actions checks are available, use their exact reported
207
+ names when updating required checks. Replace only the `ci/circleci:` entries;
208
+ preserve `GitGuardian Security Checks`, strict up-to-date checks, and any
209
+ unrelated protections. Include all three Ruby builds, RuboCop, YARD, and
210
+ `upload-coverage`. Verify a failing test or missing coverage input blocks
211
+ merging before retiring CircleCI checks.
212
+
213
+ For the Ruby support update, also replace required checks for Ruby 2.7, 3.0,
214
+ 3.1, and 3.2 with the reported checks for Ruby 3.3, 3.4, and 4.0. Keep RuboCop,
215
+ YARD, coverage, and unrelated protections required. Update protection only
216
+ after the new checks have run successfully.
217
+ 3. Prepare the Trusted Publisher and protected `release` environment. Stop and
218
+ drain CircleCI publishing before pushing the release workflow to `release`.
219
+ Run the first real publish only as an explicitly requested release task;
220
+ first inspect existing tags and RubyGems versions, including any partial release.
221
+ 4. Stop and drain the CircleCI Challenge schedule before merging the Actions
222
+ Challenge workflow into `master`. Run the Challenge dry run, then check an
223
+ actual generated PR and its CI before considering this step complete.
224
+ 5. Stop the CircleCI project and confirm no queued or running jobs remain. Remove
225
+ obsolete CircleCI credentials and integration only after confirming they are
226
+ not shared with other projects. Keep the migration commit available for
227
+ restoring the previous configuration if needed.
228
+
229
+ For rollback, [disable the corresponding Actions workflow](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/disable-and-enable-workflows)
230
+ in the repository's Actions UI (or use `gh workflow disable release.yml` or
231
+ `gh workflow disable rubocop_challenge.yml`) and wait for any running writes to
232
+ finish before restoring the corresponding CircleCI writer. Inspect any partial
233
+ publication before proceeding. Restore previously recorded
234
+ required checks together with the previous CircleCI configuration and keep at
235
+ least one complete CI path active. Never enable both
236
+ publishers or both Challenge writers at once. Do not delete existing release tags
237
+ or retry publishing blindly after a partial failure.