Add BUNDLE_GEMFILE-aware dual-boot support (rebased on v359)#4
Add BUNDLE_GEMFILE-aware dual-boot support (rebased on v359)#4JuanVqz wants to merge 12 commits into
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
| # 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.
Bundler does not resolve the Gemfile.next symlink: File.basename(__FILE__) and the lockfile name both resolve to Gemfile.next across Bundler 2.1.4, 2.5.17, 2.5.22, 2.6.6, and 4.0.9. The dual-boot trick works with the symlink as-is. The earlier staging failures were the BUNDLE_GEMFILE env-dir ordering bug (fixed separately), not the symlink, so this materializer is unnecessary.
The integration-test job needs Heroku API secrets only present on the upstream repo, and lint runs locally. Drop the workflow in this fork rather than carry a fork-only conditional.
CHANGELOG.md tracks the upstream buildpack releases; fork-specific notes do not belong here. Documented in the README instead.
Trim the inherited upstream content (which drifts and confuses) and link to the official buildpack instead. State the fork's purpose, document the BUNDLE_GEMFILE dual-boot usage, point to the next-only sibling fork, and add a top-level warning that switching BUNDLE_GEMFILE needs a rebuild, not just a config change, or a Procfile release command fails on the uninstalled version.
|
|
||
| ## Documentation | ||
| ```sh | ||
| heroku buildpacks:set https://github.com/fastruby/heroku-buildpack-ruby#use_gemfile_next_v359 -a <app> |
There was a problem hiding this comment.
@arielj are we going to merge this PR or keep it as the previous version? if so, we need to update this to main or probably just the URL will pickup the default branch.
This fork does not maintain its own CHANGELOG (it tracks upstream's), so the upstream convention check that requires every PR to touch CHANGELOG.md does not apply and would block all fork PRs.
document_ruby_version, hatchet_app_cleaner, and prepare-release are upstream release/maintenance automation that needs upstream secrets and conventions. They do not apply to this fork.
|
@arielj, I want your opinion on the release process. AI suggested 3 options: Option A: rebase a thin patch per upstream release (recommended)
Sync flow:
Option B: merge upstream into a long-lived branch Keep one permanent gemfile-next branch, git merge upstream/main into it each sync.
Option C: automate the sync A scheduled GitHub Action (or local bin/sync-upstream script) that fetches upstream, fast-forwards main, rebases the patch, opens a PR.
My recommendation Option A, plus a small local bin/sync-upstream helper so the rebase dance is one command. It matches your existing per-version workflow, keeps the patch reviewable, and the tiny footprint means rebase |
|
I think we should do option A, main branch being the latest official buildpack code + some patches, and we tag versions so they can be used in heroku |
| > - Heroku reverts the config var change because the release failed. | ||
| > - In the latest version of this fork the re-release no longer fails, but the env change is **not actually applied at runtime** either, so Heroku reports the var changed while the app keeps running the previous version. Confusing, but expected. |
There was a problem hiding this comment.
not sure if these 2 lines are needed, or maybe they can be confusing
someone using this version might not understand what's this referring to since they won't experience the failure/revert
I think that we can say something like...
When changing an environment variable, Heroku will trigger a re-release. In some cases, if the re-release fails, Heroku might revert the value to the previous one. If that happens for your app, we recommend using that supports deploying the next version without the use of the BUNDLE_GEMFILE variable.
There was a problem hiding this comment.
Updated, is this any better?
Co-authored-by: arielj <ariel@ombulabs.com>
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) works as-is: Bundler does not resolve the symlink, soFile.basename(__FILE__)and the lockfile name both resolve toGemfile.next. Verified across Bundler 2.1.4, 2.5.17, 2.5.22, 2.6.6, and 4.0.9. No symlink materialization is needed.BUNDLE_GEMFILEneeds a rebuild, not just a config changeHeroku triggers a re-release but not a rebuild when a config var changes, and the build is what installs gems. So flipping
BUNDLE_GEMFILEalone never installs the other Rails version's gems.If an app built with Rails 6.1 has its
BUNDLE_GEMFILEchanged toGemfile.next(Rails 7.0) without a redeploy, and it has arelease:command in itsProcfile, the release phase runs against Rails 7.0 whose gems are not in the slug, so it fails and Heroku reverts the config var. In the latest version of this fork the re-release no longer fails, but the change is not applied at runtime either, so Heroku reports the var changed while the app keeps running the previous version.Always change
BUNDLE_GEMFILEand then deploy, so the correct gems are installed at build time. This is documented at the top of the README.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:In Settings → Config Vars, add
BUNDLE_GEMFILEwith valueGemfile.next, then click Add. Heroku may take a moment to apply.In the Overview tab, confirm the new config var is listed.
Trigger a deploy (push to the Heroku remote, or use Deploy → Manual deploy for GitHub-connected apps). The build log should install gems for
Gemfile.next.lock.Once deployed, verify the running version:
To flip back to the current Gemfile:
heroku config:unset BUNDLE_GEMFILE -a <app>Gemfile.lock). The buildpack behaves like stockheroku/rubyagain. A config-var change alone is not enough (see the known limitation above).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.