Motivation
The gem repos (simplecov-sorbet, ast-transform, rspock) can't use dev's Gemfile generation today — their dependencies.rb is toolchain-only (ruby "4.0.6" and nothing else) and the Gemfile stays hand-written. Two gaps in BundlerRepository block adoption:
- No
gemspec directive. A packaged gem's runtime dependencies must stay single-sourced in the gemspec; its Gemfile needs a gemspec line so bundle resolves the gem under development plus its declared deps. The generated Gemfile can only render gem lines.
- The
ruby directive is unconditionally the exact toolchain pin. write_gemfile emits ruby "<exact version>" whenever dependencies.rb declares a Ruby. For an app like ai-flow that's correct: CI runs a single Ruby, and reading .ruby-version in setup-ruby keeps the interpreter aligned with the pin, so bundler's Gemfile check passes. Gems are structurally different: their CI matrix runs 3.3 / 3.4 / 4.0 on purpose — proving the gem works on Rubies other than the development pin. There is no single version to align to, so an exact ruby directive fails bundle install on every non-pinned matrix row ("Your Ruby version is 3.3.x, but your Gemfile specified 4.0.6"), regardless of workflow configuration.
Design
DSL (Dev::Deps::DSL): a gemspec declaration:
Dev::Deps.define do
ruby "4.0.6"
gemspec
group(:development) do
gem "rubocop-shopify"
# ...
end
end
Generated Gemfile (BundlerRepository.write_gemfile):
- Emit
gemspec after source.
- When
gemspec is declared, omit the ruby directive entirely. The version pin does not disappear — it moves to the layers where it belongs:
.ruby-version + shadowenv (dev-provisioned) keep local development and dev commands on the exact toolchain pin;
- the gemspec's
required_ruby_version expresses the supported range (the contract the CI matrix verifies);
- the Gemfile asserts nothing, so every matrix row installs cleanly.
Locking/staleness:
bundle lock reads the gemspec natively, so resolution needs no special handling.
- Gemspec dependency edits are a resolution input that
dependencies.rb's manifest digest doesn't capture. Options: hash the gemspec into the staleness digest, or rely on the frozen install (BUNDLE_FROZEN=true) failing loudly on gemspec/lock drift — which it already does — with a message pointing at dev update-deps.
Adoption
simplecov-sorbet, ast-transform, rspock: move dev/test gems from the hand-written Gemfile into dependencies.rb groups, delete the hand-written Gemfile, run dev update-deps. Runtime deps stay in the gemspec, unchanged. Contributors without dev keep working — the generated Gemfile/Gemfile.lock are committed, so plain bundle install still works.
Non-goals
Motivation
The gem repos (
simplecov-sorbet,ast-transform,rspock) can't use dev's Gemfile generation today — theirdependencies.rbis toolchain-only (ruby "4.0.6"and nothing else) and the Gemfile stays hand-written. Two gaps inBundlerRepositoryblock adoption:gemspecdirective. A packaged gem's runtime dependencies must stay single-sourced in the gemspec; its Gemfile needs agemspecline sobundleresolves the gem under development plus its declared deps. The generated Gemfile can only rendergemlines.rubydirective is unconditionally the exact toolchain pin.write_gemfileemitsruby "<exact version>"wheneverdependencies.rbdeclares a Ruby. For an app like ai-flow that's correct: CI runs a single Ruby, and reading.ruby-versioninsetup-rubykeeps the interpreter aligned with the pin, so bundler's Gemfile check passes. Gems are structurally different: their CI matrix runs 3.3 / 3.4 / 4.0 on purpose — proving the gem works on Rubies other than the development pin. There is no single version to align to, so an exactrubydirective failsbundle installon every non-pinned matrix row ("Your Ruby version is 3.3.x, but your Gemfile specified 4.0.6"), regardless of workflow configuration.Design
DSL (
Dev::Deps::DSL): agemspecdeclaration:Generated Gemfile (
BundlerRepository.write_gemfile):gemspecaftersource.gemspecis declared, omit therubydirective entirely. The version pin does not disappear — it moves to the layers where it belongs:.ruby-version+ shadowenv (dev-provisioned) keep local development anddevcommands on the exact toolchain pin;required_ruby_versionexpresses the supported range (the contract the CI matrix verifies);Locking/staleness:
bundle lockreads the gemspec natively, so resolution needs no special handling.dependencies.rb's manifest digest doesn't capture. Options: hash the gemspec into the staleness digest, or rely on the frozen install (BUNDLE_FROZEN=true) failing loudly on gemspec/lock drift — which it already does — with a message pointing atdev update-deps.Adoption
simplecov-sorbet,ast-transform,rspock: move dev/test gems from the hand-written Gemfile intodependencies.rbgroups, delete the hand-written Gemfile, rundev update-deps. Runtime deps stay in the gemspec, unchanged. Contributors without dev keep working — the generated Gemfile/Gemfile.lock are committed, so plainbundle installstill works.Non-goals
dev releasewith pluggable publishers #70).rubydirective — correct for single-Ruby CI).