With Ruby shipping a new minor version every year, how would you set an upgrade cadence and policy for dozens of Ruby services?
answer
- one feature release a year
- teeny releases carry fixes
- maintenance windows end
- small yearly steps vs big jumps
- CI on the next Ruby early
basics
~20 sPlan one upgrade per year per service instead of multi-version jumps: test every service against the new release early, adopt it within a set window after a patch release, and never let a service fall off security maintenance.
solid answer
~50 sRuby ships one feature release a year (3.3, 3.4, 4.0) and teeny releases with fixes in between; each branch is maintained for a limited time and then only gets security fixes before its end of life. For a fleet I would set a policy with three parts. **Early signal:** a shared CI job runs every service against the newest release, or its previews, with deprecation warnings failing the build. **Adoption window:** services move to a new minor within a fixed window, often after its first teeny release, so gems have caught up, and never later than the previous branch's end of security maintenance. **Shared paving:** one team maintains base images, a supported-versions list and a changelog of known breaks such as bundled-gem moves. The trade-off is steady small cost versus rare, risky multi-version jumps; I prefer the small yearly cost.
code
ruby · 8 lines# Report the runtime at boot so a fleet dashboard can show who is behind
runtime = {
ruby: RUBY_VERSION,
engine: RUBY_ENGINE,
rubygems: Gem::VERSION,
supported: Gem.ruby_version >= Gem::Version.new("3.4")
}
$stdout.puts("runtime #{runtime}")go deeper
Recall that Ruby ships one feature release a year and teeny releases with fixes in between.
Explain maintenance windows and why falling several releases behind makes each upgrade larger.
Run upgrades as routine work: CI on the next Ruby, deprecations failing the build, and a staged rollout for critical services.
Own the fleet policy: choose track-latest or N-1 with a deadline tied to security maintenance, fund a paved road, and govern exceptions with owners and dates.
## The rhythm you are planning around Ruby's release line is regular. A **minor** release (3.3, 3.4, 4.0) ships **once a year**; Ruby's own headers describe the minor number as changing annually, and 4.0 is simply that year's release, not a rewrite. Between minors, **teeny** releases (4.0.1 ... 4.0.7) carry bug and security fixes. Each branch is maintained for a limited period, then receives security fixes only, then reaches end of life; the exact dates are published by the Ruby core team and should be checked, not assumed. For one service, that makes an upgrade a yearly chore. For dozens of services, it is a **policy question**: how far behind may a service fall, who pays for the upgrade, and what signal says it is safe? ## Options and their trade-offs | Policy | Upside | Downside | |---|---|---| | Track latest within a window | small yearly changes; newest performance work | steady engineering cost every year | | Stay one minor behind (N-1) | gems have caught up; fewer surprises | still yearly work, slightly older features | | Stay on the oldest maintained branch | least frequent work | every upgrade spans several releases and races a deadline | | Upgrade only when forced | no planned work | big-bang jumps, security exposure, lost expertise | Most mature teams land between the first two. The last two concentrate risk: a jump across several releases stacks removals, library moves and format changes into one change, usually under the pressure of an end-of-life date. ## A policy that works in practice 1. **Continuous early signal.** A shared CI job runs every service's suite on the newest Ruby, and on previews once they appear, without blocking merges. It turns "what will break" into a list months ahead. 2. **Deprecations fail the build.** Services run tests with `-W:deprecated` and a `Warning.warn` extension that raises, so removals are fixed while they are still warnings. 3. **An adoption window.** For example: every service moves to a new minor within a fixed number of months of its release, typically after the first teeny release so the gem ecosystem has caught up, and in any case well before the previous branch leaves security maintenance. 4. **Paved road.** One platform team owns base images, the supported-versions list, the build of native extensions in CI, and a running list of known breaks per release, such as which libraries moved to bundled gems. 5. **Visibility.** Each service reports its `RUBY_VERSION` at boot or in its build metadata, so a dashboard shows who is behind without asking. ## Judgement calls a lead owns - **Exceptions.** A service with a hard dependency that has not caught up may stay behind, but with an owner, a reason and a date, not indefinitely. - **Order.** Upgrade low-risk internal services first to harvest fixes, then the critical ones, like payroll, with the playbook already proven. - **Budget.** A yearly upgrade is cheaper per service when it is routine; treating it as a project every few years is what makes it expensive. - **Experiments.** Features marked experimental, such as Ractor or ZJIT in 4.0, stay out of production policy until the release notes drop the label. ## Measuring whether the policy works - **Version spread:** how many minors the fleet spans today. A healthy fleet usually spans two; four is a warning sign. - **Lag:** median time from a release to a service's production adoption. - **Upgrade size:** the number of code changes per upgrade. Small, routine upgrades mean the early-signal CI job and deprecation rule are doing their job. - **Exceptions:** count and age of services held back, each with an owner and a review date. ## What interviewers listen for A principal answer names the yearly cadence and maintenance windows, chooses a policy with explicit trade-offs, and backs it with mechanisms: early CI, deprecations as errors, a window with a deadline, shared paving and visibility.
- Why wait for the first teeny release instead of adopting a .0 on day one?A new minor often needs a few weeks for gems with native extensions and for early bug fixes to land in a teeny release. Waiting for x.y.1 trades a small delay for fewer surprises. The early-signal CI job means the work is already known, so the wait does not turn into drift.
- What do you do with a service that cannot upgrade because a key gem is incompatible?Record it as an exception with an owner and a review date, contribute or sponsor the fix upstream if the gem matters, and plan a replacement if it is abandoned. The one thing not to do is let the service quietly stay on a branch that leaves security maintenance.
saying these in an interview costs you the question
- Ruby versions are supported indefinitely, so there is no deadline to upgrade
- A 4.0 major bump means the language was rewritten and every service must be rebuilt
- Skipping several releases and upgrading once is cheaper than yearly steps
- Upgrades can wait until something breaks in production
- Experimental features become production-ready as soon as they ship in a release