Add BUNDLE_GEMFILE-aware dual-boot support (rebased on v359)#4
Conversation
Rebases the spirit of PR #3 (use_gemfile_next) onto upstream v359. All buildpack phases now operate on Gemfile.next / Gemfile.next.lock instead of the default Gemfile / Gemfile.lock. Spots patched: - lib/language_pack.rb: top-level lockfile detection reads Gemfile.next.lock (drives bundler version and ruby version resolution). - lib/language_pack/helpers/bundler_wrapper.rb: default gemfile_path is ./Gemfile.next. - lib/language_pack/ruby.rb: * self.use? detects by Gemfile.next presence. * setup_language_pack_environment sets ENV['BUNDLE_GEMFILE'] for in-process build steps. * setup_export exports BUNDLE_GEMFILE for subsequent buildpacks. * setup_profiled sets the runtime override so dynos boot Gemfile.next. * build_bundler installs gems for Gemfile.next. * rake_env injects BUNDLE_GEMFILE into the rake subprocess (this is the spot the original PR #3 missed, which caused the rake-task detection step to fall back to Gemfile.lock).
When an app uses the common next_rails dual-boot setup with Gemfile.next
as a symlink to Gemfile, the __FILE__-based 'next?' check can resolve
inconsistently across Bundler versions: older Bundler keeps __FILE__ as
the symlink path ('Gemfile.next'), newer Bundler may dereference it to
'Gemfile', silently flipping the dual-boot conditional and installing
the wrong Rails version.
Rewrite the symlink to a real file containing the same contents at the
start of build_bundler. The repo keeps the symlink for the developer
workflow; only the buildpack's working copy is changed.
Replace the hardcoded 'Gemfile.next' references with a single source of truth: LanguagePack.gemfile_name, which reads ENV['BUNDLE_GEMFILE'] and returns its basename, defaulting to 'Gemfile' when unset. Lockfile name is derived as '<gemfile>.lock'. With this change, the fork acts as a drop-in replacement for the stock heroku/ruby buildpack when BUNDLE_GEMFILE is not set, and switches to a next_rails-style alternative (e.g. Gemfile.next) when the env var is set via a Heroku config var. The symlink materializer also operates on the chosen Gemfile generically.
Heroku passes user-set config vars to buildpacks via the env dir (ARGV[2] of ruby_compile), not by setting ENV directly. The previous implementation read ENV['BUNDLE_GEMFILE'] before calling initialize_env, so the value was always empty and gemfile_name defaulted to 'Gemfile' even when the user had set BUNDLE_GEMFILE=Gemfile.next. Two fixes: - ruby_compile.rb: call initialize_env before LanguagePack.gemfile_lock so the env dir is loaded before we touch any lockfile. - LanguagePack.gemfile_name: fall back to user_env_hash when ENV is empty so the value is visible to all phases of the build, not just those that pipe with user_env: true.
- Gate the integration-test job on github.repository == upstream so forks do not fail on a job that requires Heroku API secrets they cannot have. - Add CHANGELOG entries describing the BUNDLE_GEMFILE wiring and the CI fork-gate. Satisfies the check-changelog CI job.
57def8f to
f8b404f
Compare
There was a problem hiding this comment.
Stop it failing in our fork.
| # read. Without this, gemfile_lock would always read Gemfile.lock regardless | ||
| # of the user's BUNDLE_GEMFILE setting. | ||
| LanguagePack::ShellHelpers.initialize_env(ARGV[2]) | ||
| gemfile_lock = LanguagePack.gemfile_lock(app_path: app_path) |
There was a problem hiding this comment.
Heroku passes user config vars to buildpacks via a directory of files (one file per var), not as actual ENV entries. The path to that dir is ARGV[2] in ruby_compile.rb.
Until LanguagePack::ShellHelpers.initialize_env(ARGV[2]) runs, that data isn't loaded anywhere.
Sequence before our fix:
gemfile_lock = LanguagePack.gemfile_lock(...) # reads ENV["BUNDLE_GEMFILE"] → ""
Dir.chdir(app_path)
LanguagePack::ShellHelpers.initialize_env(...) # now user_env_hash["BUNDLE_GEMFILE"] = "Gemfile.next"LanguagePack.gemfile_name (which gemfile_lock calls via lockfile_name) reads:
ENV["BUNDLE_GEMFILE"] # empty, Heroku doesn't set it in ENV
|| LanguagePack::ShellHelpers.user_env_hash[..] # empty too, initialize_env hasn't run yet
|| "Gemfile" # falls through to defaultSo it picked Gemfile.lock. The bundler/Ruby version detection then used the wrong lockfile, and build_bundler later built against Gemfile. Meanwhile bundle list (called with user_env: true) read user_env_hash["BUNDLE_GEMFILE"] = Gemfile.next and tried to verify gems from Gemfile.next.lock which had never been installed. That's the exact mismatch the failed build exposed.
After moving the line below initialize_env, user_env_hash is populated first, gemfile_name resolves to Gemfile.next, and every later phase agrees on the same lockfile.
Summary
Adds dual-boot (
next_rails) support on top of upstream v359. The buildpack reads theBUNDLE_GEMFILEenv var (typically a Heroku config var) to decide which Gemfile drives the build. WhenBUNDLE_GEMFILEis unset, the buildpack behaves like stockheroku/ruby, making this a drop-in replacement.Concretely:
BUNDLE_GEMFILEunset → usesGemfileandGemfile.lock(default Heroku behavior).BUNDLE_GEMFILE=Gemfile.next→ usesGemfile.nextandGemfile.next.lockfor the build, including bundler/ruby version detection,bundle install,bundle clean,bundle list, rake task detection, and runtime dyno boot.A
Gemfile.nextthat is a symlink toGemfile(the commonnext_railssetup) is materialized into a real file during the build so theFile.basename(__FILE__)dual-boot trick works deterministically across Bundler versions. The repo keeps the symlink for the developer workflow.Deployment steps
BUNDLE_GEMFILE=Gemfile.next bundle exec rails -vshowing the next Rails version.heroku buildpacks:set https://github.com/fastruby/heroku-buildpack-ruby#use_gemfile_next_v359 -a <app>.heroku config:set BUNDLE_GEMFILE=Gemfile.next -a <app>.heroku repo:purge_cache -a <app>(requiresheroku-repoplugin).git push heroku <branch>:main.heroku run "bin/rails --version" -a <app>should show the next Rails version.To flip back:
heroku config:unset BUNDLE_GEMFILE -a <app>and redeploy.QA notes
Pick a project that already has the dual-boot configured and running locally.
In Heroku, go to Settings → Buildpacks. Remove
heroku/ruby(or any previous fork branch) and add:(image placeholder: buildpacks panel showing the new branch URL)
In Settings → Config Vars, add
BUNDLE_GEMFILEwith valueGemfile.next, then click Add. Heroku may take a moment to apply.(image placeholder: config vars panel)
In the Overview tab, confirm the new config var is listed.
(image placeholder: overview showing BUNDLE_GEMFILE)
Trigger a deploy (push to the Heroku remote, or use Deploy → Manual deploy for GitHub-connected apps). The build log should include the line
Materializing Gemfile.next symlink -> Gemfile(only if your app uses aGemfile.nextsymlink) and then install gems forGemfile.next.lock.Once deployed, verify the running version:
To flip back to the current Gemfile without rebuilding from scratch:
heroku config:unset BUNDLE_GEMFILE -a <app>heroku repo:purge_cache -a <app>heroku/rubyagain and installsGemfile.lock.Comparison vs the previous fork branches
add_gemfile_next_support(2024)use_gemfile_next_v359(this PR)BUNDLE_GEMFILEenv varBUNDLE_GEMFILE, defaults toGemfilerake_envBUNDLE_GEMFILEinjectioninitialize_envbefore lockfile readuser_env_hashWhat changed upstream between v270 (Apr 2024) and v359 (May 2026)
The rebase brings in 89 releases of fixes and new features. Highlights relevant to Ruby/Rails/bundler:
Bundler
BUNDLED WITH 4.0.xnow receive Bundler 4.0).BUNDLED WITHexactly. Previously the buildpack mapped to a "known good" version (e.g.2.7.xwould always install2.7.2).gem install bundlerinstead of being pre-built and downloaded from S3.2.3.25to2.5.23. Ruby's stdlib bundler takes precedence when greater.BUNDLED WITHwritten with 2-space indent (newer bundler format).Ruby
Gemfile.lockinstead of being detected by runningbundle platform --ruby. As a result, Ruby is now installed before Bundler.Stack and platform
heroku-26stack support.24.13.0.DATABASE_URLwhen adapter is unknown.PUMA_PERSISTENT_TIMEOUT=95to match Router 2.0 recommendations. Warns on Puma < 7.0.0, errors on Puma 7.0.0 to 7.0.2.Removed / cleaned up
Net effect for this PR
Gemfile.lock(or in our caseGemfile.next.lock), so the dual-boot wiring threads through cleanly without needing to reproduce the oldbundle platform --rubyshell-out.