From a034ccbcac75fc4821c36fce68679c1c80601411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Mon, 25 May 2026 14:24:53 -0600 Subject: [PATCH 01/12] Hardcode Gemfile.next as the gemfile for dual-boot deploys 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). --- lib/language_pack.rb | 4 ++-- lib/language_pack/helpers/bundler_wrapper.rb | 2 +- lib/language_pack/ruby.rb | 13 +++++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/language_pack.rb b/lib/language_pack.rb index 0e4470a9e..1dad7725f 100644 --- a/lib/language_pack.rb +++ b/lib/language_pack.rb @@ -10,13 +10,13 @@ module Helpers end def self.gemfile_lock(app_path:) - path = app_path.join("Gemfile.lock") + path = app_path.join("Gemfile.next.lock") if path.exist? LanguagePack::Helpers::GemfileLock.new( contents: path.read ) else - raise BuildpackError.new("Gemfile.lock required. Please check it in.") + raise BuildpackError.new("Gemfile.next.lock required. Please check it in.") end end diff --git a/lib/language_pack/helpers/bundler_wrapper.rb b/lib/language_pack/helpers/bundler_wrapper.rb index 386118518..ae75ddbfd 100644 --- a/lib/language_pack/helpers/bundler_wrapper.rb +++ b/lib/language_pack/helpers/bundler_wrapper.rb @@ -36,7 +36,7 @@ class LanguagePack::Helpers::BundlerWrapper def initialize( bundler_path:, bundler_version:, - gemfile_path: Pathname.new("./Gemfile"), + gemfile_path: Pathname.new("./Gemfile.next"), report: HerokuBuildReport::GLOBAL ) @report = report diff --git a/lib/language_pack/ruby.rb b/lib/language_pack/ruby.rb index ab5452133..54623671d 100644 --- a/lib/language_pack/ruby.rb +++ b/lib/language_pack/ruby.rb @@ -19,7 +19,7 @@ class LanguagePack::Ruby < LanguagePack::Base # detects if this is a valid Ruby app # @return [Boolean] true if it's a Ruby app def self.use?(bundler: nil) - File.exist?("Gemfile") + File.exist?("Gemfile.next") end def initialize(...) @@ -306,6 +306,7 @@ def self.setup_language_pack_environment(app_path:, ruby_version:, bundle_defaul ENV["BUNDLE_PATH"] = "vendor/bundle" ENV["BUNDLE_BIN"] = "vendor/bundle/bin" ENV["BUNDLE_DEPLOYMENT"] = "1" + ENV["BUNDLE_GEMFILE"] = app_path.join("Gemfile.next").to_s end # Sets up the environment variables for subsequent processes run by @@ -331,6 +332,7 @@ def setup_export(app_path:, ruby_version:, default_config_vars:) set_export_default "BUNDLE_WITHOUT", ENV["BUNDLE_WITHOUT"] set_export_default "BUNDLE_BIN", ENV["BUNDLE_BIN"] set_export_default "BUNDLE_DEPLOYMENT", ENV["BUNDLE_DEPLOYMENT"] # Unset on windows since we delete the Gemfile.lock + set_export_default "BUNDLE_GEMFILE", ENV["BUNDLE_GEMFILE"] default_config_vars.each do |key, value| set_export_default key, value end @@ -375,6 +377,7 @@ def setup_profiled(ruby_layer_path:, gem_layer_path:, ruby_version:, default_con set_env_default "BUNDLE_WITHOUT", ENV["BUNDLE_WITHOUT"] set_env_default "BUNDLE_BIN", ENV["BUNDLE_BIN"] set_env_default "BUNDLE_DEPLOYMENT", ENV["BUNDLE_DEPLOYMENT"] if ENV["BUNDLE_DEPLOYMENT"] # Unset on windows since we delete the Gemfile.lock + set_env_override "BUNDLE_GEMFILE", "Gemfile.next" end def warn_outdated_ruby @@ -640,7 +643,7 @@ def self.build_bundler(app_path:, io:, bundler_cache:, bundler_version:, bundler io.topic("Installing dependencies using bundler #{bundler_version}") env_vars = {} - env_vars["BUNDLE_GEMFILE"] = app_path.join("Gemfile").to_s + env_vars["BUNDLE_GEMFILE"] = app_path.join("Gemfile.next").to_s env_vars["BUNDLE_CONFIG"] = app_path.join(".bundle/config").to_s env_vars["NOKOGIRI_USE_SYSTEM_LIBRARIES"] = "true" env_vars["BUNDLE_DISABLE_VERSION_CHECK"] = "true" @@ -709,11 +712,13 @@ def rake end def rake_env - if database_url + base = if database_url {"DATABASE_URL" => database_url} else {} - end.merge(user_env_hash) + end + base["BUNDLE_GEMFILE"] = app_path.join("Gemfile.next").to_s + base.merge(user_env_hash) end def database_url From 48f102024ea211afa41de942d4fb3570955b5cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Mon, 25 May 2026 14:30:05 -0600 Subject: [PATCH 02/12] Materialize Gemfile.next symlink into a real file at build time 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. --- lib/language_pack/ruby.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/language_pack/ruby.rb b/lib/language_pack/ruby.rb index 54623671d..413b3d232 100644 --- a/lib/language_pack/ruby.rb +++ b/lib/language_pack/ruby.rb @@ -620,7 +620,29 @@ def self.remove_vendor_bundle(app_path:) end # runs bundler to install the dependencies + # If Gemfile.next is a symlink (a common next_rails dual-boot setup where + # Gemfile.next -> Gemfile), materialize it as a real file so that + # `File.basename(__FILE__)` inside the Gemfile resolves to "Gemfile.next" + # regardless of how the active Bundler version handles symlinks. The repo + # keeps the symlink for the developer workflow; the buildpack only rewrites + # it for the duration of this build. + def self.materialize_gemfile_next_symlink(app_path:, io:) + gemfile_next = app_path.join("Gemfile.next") + return unless gemfile_next.symlink? + + target = File.readlink(gemfile_next.to_s) + target_path = gemfile_next.dirname.join(target) + return unless target_path.exist? + + io.topic("Materializing Gemfile.next symlink -> #{target}") + contents = target_path.read + gemfile_next.delete + gemfile_next.write(contents) + end + def self.build_bundler(app_path:, io:, bundler_cache:, bundler_version:, bundler_output:, ruby_version:) + materialize_gemfile_next_symlink(app_path: app_path, io: io) + if app_path.join(".bundle/config").exist? warn(<<~WARNING, inline: true) You have the `.bundle/config` file checked into your repository From d18d9498e6ba556f9f4d54f47b7cf4b8a4a42f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Mon, 25 May 2026 14:56:50 -0600 Subject: [PATCH 03/12] Honor BUNDLE_GEMFILE env var so the buildpack works for current and next 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 '.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. --- lib/language_pack.rb | 18 ++++++++- lib/language_pack/helpers/bundler_wrapper.rb | 2 +- lib/language_pack/ruby.rb | 42 ++++++++++---------- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/lib/language_pack.rb b/lib/language_pack.rb index 1dad7725f..3e43a6965 100644 --- a/lib/language_pack.rb +++ b/lib/language_pack.rb @@ -9,14 +9,28 @@ module LanguagePack module Helpers end + # Name of the Gemfile this buildpack should use. Honors the BUNDLE_GEMFILE + # env var (typically set via a Heroku config var) so a user can flip between + # the current and next Gemfile without changing buildpack URLs. Falls back + # to "Gemfile", the same default as the stock heroku/ruby buildpack, making + # this a drop-in replacement when BUNDLE_GEMFILE is not set. + def self.gemfile_name + raw = ENV["BUNDLE_GEMFILE"].to_s + raw.empty? ? "Gemfile" : File.basename(raw) + end + + def self.lockfile_name + "#{gemfile_name}.lock" + end + def self.gemfile_lock(app_path:) - path = app_path.join("Gemfile.next.lock") + path = app_path.join(lockfile_name) if path.exist? LanguagePack::Helpers::GemfileLock.new( contents: path.read ) else - raise BuildpackError.new("Gemfile.next.lock required. Please check it in.") + raise BuildpackError.new("#{lockfile_name} required. Please check it in.") end end diff --git a/lib/language_pack/helpers/bundler_wrapper.rb b/lib/language_pack/helpers/bundler_wrapper.rb index ae75ddbfd..616b4b230 100644 --- a/lib/language_pack/helpers/bundler_wrapper.rb +++ b/lib/language_pack/helpers/bundler_wrapper.rb @@ -36,7 +36,7 @@ class LanguagePack::Helpers::BundlerWrapper def initialize( bundler_path:, bundler_version:, - gemfile_path: Pathname.new("./Gemfile.next"), + gemfile_path: Pathname.new("./#{LanguagePack.gemfile_name}"), report: HerokuBuildReport::GLOBAL ) @report = report diff --git a/lib/language_pack/ruby.rb b/lib/language_pack/ruby.rb index 413b3d232..161557e11 100644 --- a/lib/language_pack/ruby.rb +++ b/lib/language_pack/ruby.rb @@ -19,7 +19,7 @@ class LanguagePack::Ruby < LanguagePack::Base # detects if this is a valid Ruby app # @return [Boolean] true if it's a Ruby app def self.use?(bundler: nil) - File.exist?("Gemfile.next") + File.exist?(LanguagePack.gemfile_name) end def initialize(...) @@ -306,7 +306,7 @@ def self.setup_language_pack_environment(app_path:, ruby_version:, bundle_defaul ENV["BUNDLE_PATH"] = "vendor/bundle" ENV["BUNDLE_BIN"] = "vendor/bundle/bin" ENV["BUNDLE_DEPLOYMENT"] = "1" - ENV["BUNDLE_GEMFILE"] = app_path.join("Gemfile.next").to_s + ENV["BUNDLE_GEMFILE"] = app_path.join(LanguagePack.gemfile_name).to_s end # Sets up the environment variables for subsequent processes run by @@ -377,7 +377,7 @@ def setup_profiled(ruby_layer_path:, gem_layer_path:, ruby_version:, default_con set_env_default "BUNDLE_WITHOUT", ENV["BUNDLE_WITHOUT"] set_env_default "BUNDLE_BIN", ENV["BUNDLE_BIN"] set_env_default "BUNDLE_DEPLOYMENT", ENV["BUNDLE_DEPLOYMENT"] if ENV["BUNDLE_DEPLOYMENT"] # Unset on windows since we delete the Gemfile.lock - set_env_override "BUNDLE_GEMFILE", "Gemfile.next" + set_env_override "BUNDLE_GEMFILE", LanguagePack.gemfile_name end def warn_outdated_ruby @@ -620,28 +620,28 @@ def self.remove_vendor_bundle(app_path:) end # runs bundler to install the dependencies - # If Gemfile.next is a symlink (a common next_rails dual-boot setup where - # Gemfile.next -> Gemfile), materialize it as a real file so that - # `File.basename(__FILE__)` inside the Gemfile resolves to "Gemfile.next" - # regardless of how the active Bundler version handles symlinks. The repo - # keeps the symlink for the developer workflow; the buildpack only rewrites - # it for the duration of this build. - def self.materialize_gemfile_next_symlink(app_path:, io:) - gemfile_next = app_path.join("Gemfile.next") - return unless gemfile_next.symlink? - - target = File.readlink(gemfile_next.to_s) - target_path = gemfile_next.dirname.join(target) + # If the active Gemfile (typically Gemfile.next in dual-boot setups) is a + # symlink, materialize it as a real file so that `File.basename(__FILE__)` + # inside the Gemfile resolves to the symlink name regardless of how the + # active Bundler version handles symlinks. The repo keeps the symlink for + # the developer workflow; the buildpack only rewrites the working copy for + # the duration of this build. + def self.materialize_gemfile_symlink(app_path:, io:) + gemfile = app_path.join(LanguagePack.gemfile_name) + return unless gemfile.symlink? + + target = File.readlink(gemfile.to_s) + target_path = gemfile.dirname.join(target) return unless target_path.exist? - io.topic("Materializing Gemfile.next symlink -> #{target}") + io.topic("Materializing #{LanguagePack.gemfile_name} symlink -> #{target}") contents = target_path.read - gemfile_next.delete - gemfile_next.write(contents) + gemfile.delete + gemfile.write(contents) end def self.build_bundler(app_path:, io:, bundler_cache:, bundler_version:, bundler_output:, ruby_version:) - materialize_gemfile_next_symlink(app_path: app_path, io: io) + materialize_gemfile_symlink(app_path: app_path, io: io) if app_path.join(".bundle/config").exist? warn(<<~WARNING, inline: true) @@ -665,7 +665,7 @@ def self.build_bundler(app_path:, io:, bundler_cache:, bundler_version:, bundler io.topic("Installing dependencies using bundler #{bundler_version}") env_vars = {} - env_vars["BUNDLE_GEMFILE"] = app_path.join("Gemfile.next").to_s + env_vars["BUNDLE_GEMFILE"] = app_path.join(LanguagePack.gemfile_name).to_s env_vars["BUNDLE_CONFIG"] = app_path.join(".bundle/config").to_s env_vars["NOKOGIRI_USE_SYSTEM_LIBRARIES"] = "true" env_vars["BUNDLE_DISABLE_VERSION_CHECK"] = "true" @@ -739,7 +739,7 @@ def rake_env else {} end - base["BUNDLE_GEMFILE"] = app_path.join("Gemfile.next").to_s + base["BUNDLE_GEMFILE"] = app_path.join(LanguagePack.gemfile_name).to_s base.merge(user_env_hash) end From 535bb1fe421e809e67eca790d064a52184058ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Mon, 25 May 2026 15:09:22 -0600 Subject: [PATCH 04/12] Read BUNDLE_GEMFILE from user config vars (env dir), not just ENV 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. --- bin/support/ruby_compile.rb | 6 +++++- lib/language_pack.rb | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bin/support/ruby_compile.rb b/bin/support/ruby_compile.rb index 036c67dad..e126728f1 100755 --- a/bin/support/ruby_compile.rb +++ b/bin/support/ruby_compile.rb @@ -17,10 +17,14 @@ begin app_path = Pathname(ARGV[0]) cache_path = Pathname(ARGV[1]) - gemfile_lock = LanguagePack.gemfile_lock(app_path: app_path) Dir.chdir(app_path) + # Load user config vars from the env dir before we touch the Gemfile so that + # BUNDLE_GEMFILE (set as a Heroku config var) can steer which lockfile we + # 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) LanguagePack.call( app_path: app_path, cache_path: cache_path, diff --git a/lib/language_pack.rb b/lib/language_pack.rb index 3e43a6965..59d9e84a2 100644 --- a/lib/language_pack.rb +++ b/lib/language_pack.rb @@ -16,6 +16,9 @@ module Helpers # this a drop-in replacement when BUNDLE_GEMFILE is not set. def self.gemfile_name raw = ENV["BUNDLE_GEMFILE"].to_s + if raw.empty? && defined?(LanguagePack::ShellHelpers) + raw = LanguagePack::ShellHelpers.user_env_hash["BUNDLE_GEMFILE"].to_s + end raw.empty? ? "Gemfile" : File.basename(raw) end From f8b404fbc46fa1be18fc05c611031641e5ef945c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Mon, 25 May 2026 15:37:23 -0600 Subject: [PATCH 05/12] CI: skip integration-test on forks, document changes in CHANGELOG - 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. --- .github/workflows/ci.yml | 4 ++++ CHANGELOG.md | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4351ae368..4b76ea82a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,10 @@ jobs: xargs -0 shellcheck --check-sourced --color=always integration-test: + # Only runs on the upstream repo, which holds the required secrets + # (HEROKU_API_KEY, etc.). Forks don't have access, so the job would + # always fail at the Hatchet setup step. + if: github.repository == 'heroku/heroku-buildpack-ruby' runs-on: ubuntu-24.04 env: HATCHET_APP_LIMIT: 300 diff --git a/CHANGELOG.md b/CHANGELOG.md index 22524b30e..c9dbd9ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +- Fork-only: honor `BUNDLE_GEMFILE` (typically a Heroku config var) so the buildpack can deploy either the current Gemfile or a next_rails-style alternative (e.g. `Gemfile.next`). Defaults to `Gemfile`, so behavior is unchanged when `BUNDLE_GEMFILE` is not set. Includes a build-time materializer that rewrites a `Gemfile.next` symlink as a real file so the `File.basename(__FILE__)` dual-boot trick works deterministically across Bundler versions. +- Fork-only: skip the `integration-test` workflow on forks (the job requires Heroku API secrets only available on the upstream repo). + ## [v359] - 2026-05-20 From 93e8262b303d22fce7c17b432091cc981fbc5c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 9 Jun 2026 13:22:25 -0600 Subject: [PATCH 06/12] Remove redundant materialize_gemfile_symlink 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. --- lib/language_pack/ruby.rb | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/lib/language_pack/ruby.rb b/lib/language_pack/ruby.rb index 161557e11..e314d5af8 100644 --- a/lib/language_pack/ruby.rb +++ b/lib/language_pack/ruby.rb @@ -620,29 +620,7 @@ def self.remove_vendor_bundle(app_path:) end # runs bundler to install the dependencies - # If the active Gemfile (typically Gemfile.next in dual-boot setups) is a - # symlink, materialize it as a real file so that `File.basename(__FILE__)` - # inside the Gemfile resolves to the symlink name regardless of how the - # active Bundler version handles symlinks. The repo keeps the symlink for - # the developer workflow; the buildpack only rewrites the working copy for - # the duration of this build. - def self.materialize_gemfile_symlink(app_path:, io:) - gemfile = app_path.join(LanguagePack.gemfile_name) - return unless gemfile.symlink? - - target = File.readlink(gemfile.to_s) - target_path = gemfile.dirname.join(target) - return unless target_path.exist? - - io.topic("Materializing #{LanguagePack.gemfile_name} symlink -> #{target}") - contents = target_path.read - gemfile.delete - gemfile.write(contents) - end - def self.build_bundler(app_path:, io:, bundler_cache:, bundler_version:, bundler_output:, ruby_version:) - materialize_gemfile_symlink(app_path: app_path, io: io) - if app_path.join(".bundle/config").exist? warn(<<~WARNING, inline: true) You have the `.bundle/config` file checked into your repository From f128dabf6b2b4bb6fd28dfb76430c250e2eb9015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 9 Jun 2026 13:22:25 -0600 Subject: [PATCH 07/12] Remove CI workflow 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. --- .github/workflows/ci.yml | 60 ---------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 4b76ea82a..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: CI - -on: - push: - # Avoid duplicate builds on PRs. - branches: - - main - pull_request: - -permissions: - contents: read - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Install Ruby - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - ruby-version: "3.3.9" - - name: Run StandardRB - run: bundle exec standardrb - - name: Run ShellCheck bin top level - run: | - # All bash files that don't end in '.rb' - # https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec - git ls-files -z --cached --others --exclude-standard ':(exclude)*.rb' 'bin/*' '*/bin/*' '*.sh' | \ - xargs -0 shellcheck --check-sourced --color=always - - integration-test: - # Only runs on the upstream repo, which holds the required secrets - # (HEROKU_API_KEY, etc.). Forks don't have access, so the job would - # always fail at the Hatchet setup step. - if: github.repository == 'heroku/heroku-buildpack-ruby' - runs-on: ubuntu-24.04 - env: - HATCHET_APP_LIMIT: 300 - HATCHET_EXPENSIVE_MODE: 1 - HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} - HEROKU_API_USER: ${{ secrets.HEROKU_API_USER }} - RAILS_LTS_CREDS: ${{ secrets.RAILS_LTS_CREDS }} - HEROKU_DISABLE_AUTOUPDATE: 1 - PARALLEL_SPLIT_TEST_PROCESSES: 85 - RSPEC_RETRY_RETRY_COUNT: 1 - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Install Ruby and dependencies - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - ruby-version: "3.3.9" - - name: Hatchet setup - run: bundle exec hatchet ci:setup - - name: Run Hatchet integration tests - # parallel_split_test runs rspec in parallel, with concurrency equal to PARALLEL_SPLIT_TEST_PROCESSES. - run: bundle exec parallel_split_test spec/ From d4dfee059834ea275d1e27b82e351ab7f2cb074f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 9 Jun 2026 13:22:25 -0600 Subject: [PATCH 08/12] Revert CHANGELOG to upstream state CHANGELOG.md tracks the upstream buildpack releases; fork-specific notes do not belong here. Documented in the README instead. --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9dbd9ecd..22524b30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,6 @@ ## [Unreleased] -- Fork-only: honor `BUNDLE_GEMFILE` (typically a Heroku config var) so the buildpack can deploy either the current Gemfile or a next_rails-style alternative (e.g. `Gemfile.next`). Defaults to `Gemfile`, so behavior is unchanged when `BUNDLE_GEMFILE` is not set. Includes a build-time materializer that rewrites a `Gemfile.next` symlink as a real file so the `File.basename(__FILE__)` dual-boot trick works deterministically across Bundler versions. -- Fork-only: skip the `integration-test` workflow on forks (the job requires Heroku API secrets only available on the upstream repo). - ## [v359] - 2026-05-20 From 7b1de7ec879022cdbcbd8a751a65e0f746b9d29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 9 Jun 2026 13:22:25 -0600 Subject: [PATCH 09/12] Rewrite README for the dual-boot fork 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. --- README.md | 100 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 3e4a55d1a..5904aab2f 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,81 @@ -# Heroku Buildpack for Ruby +# Heroku Buildpack for Ruby (next_rails dual-boot fork) + ![ruby](https://raw.githubusercontent.com/heroku/buildpacks/refs/heads/main/assets/images/buildpack-banner-ruby.png) -This is a [Heroku Buildpack](http://devcenter.heroku.com/articles/buildpacks) for Ruby, Rack, and Rails apps. It uses [Bundler](https://bundler.io) for dependency management. +This is a fork of [Heroku's official Ruby buildpack](https://github.com/heroku/heroku-buildpack-ruby). The **only** thing it adds is dual-boot support through the `BUNDLE_GEMFILE` environment variable. For all standard Ruby, Rack, and Rails buildpack behavior, and for full documentation, use the official buildpack and the [Heroku Ruby Support](https://devcenter.heroku.com/articles/ruby-support) docs. -This buildpack requires 64-bit Linux. +> ## ⚠️ Read this first: changing `BUNDLE_GEMFILE` requires a rebuild, not just a config change +> +> Heroku triggers a **re-release but not a rebuild** when you change a config var. The build is what installs gems, so flipping `BUNDLE_GEMFILE` on its own never installs the other Rails version's gems. +> +> Example: an app built with Rails 6.1 only has 6.1 gems in its slug. If you then change `BUNDLE_GEMFILE` to `Gemfile.next` (Rails 7.0): +> +> - Heroku runs a new release with the changed env but does **not** rebuild the slug. +> - If you have a `release:` command in your `Procfile`, it runs against Rails 7.0, whose gems are not in the slug, so it **fails**. +> - 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. +> +> **To switch versions: change `BUNDLE_GEMFILE` and then trigger a deploy (e.g. push a commit) so the correct gems are installed at build time.** Do not rely on flipping the config var by itself. -## Usage +## What this fork is for -### Ruby +It lets a single app switch between its current Gemfile and a [`next_rails`](https://github.com/fastruby/next_rails)-style alternative (e.g. `Gemfile.next`) at deploy time. `next_rails` is maintained by [FastRuby.io](https://www.fastruby.io). -Example Usage: +- `BUNDLE_GEMFILE` **unset** → behaves exactly like the official `heroku/ruby` buildpack (uses `Gemfile` / `Gemfile.lock`). Drop-in replacement. +- `BUNDLE_GEMFILE=Gemfile.next` → builds and runs against `Gemfile.next` / `Gemfile.next.lock`. - $ ls - Gemfile Gemfile.lock +## Usage - $ heroku create --buildpack heroku/ruby +1. Confirm the app boots locally on the next version: - $ git push heroku main - ... - -----> Heroku receiving push - -----> Fetching custom buildpack - -----> Ruby app detected - -----> Installing dependencies using Bundler version 1.1.rc - Running: bundle install --without development:test --path vendor/bundle --deployment - Fetching gem metadata from http://rubygems.org/.. - Installing rack (1.3.5) - Using bundler (1.1.rc) - Your bundle is complete! It was installed into ./vendor/bundle - Cleaning up the bundler cache. - -----> Discovering process types - Procfile declares types -> (none) - Default types for Ruby -> console, rake + ```sh + BUNDLE_GEMFILE=Gemfile.next bundle exec rails -v + ``` -The buildpack will detect your app as Ruby if it has a `Gemfile` and `Gemfile.lock` files in the root directory. It will then proceed to run `bundle install` after setting up the appropriate environment for [ruby](http://ruby-lang.org) and [Bundler](https://bundler.io). +2. Point the app at this fork (pin to a branch or tag): -## Documentation + ```sh + heroku buildpacks:set https://github.com/fastruby/heroku-buildpack-ruby#use_gemfile_next_v359 -a + ``` -For more information about using Ruby and buildpacks on Heroku, see these Dev Center articles: +3. Set the config var: -- [Heroku Ruby Support](https://devcenter.heroku.com/articles/ruby-support) -- [Getting Started with Ruby on Heroku](https://devcenter.heroku.com/articles/getting-started-with-ruby) -- [Getting Started with Rails 7 on Heroku](https://devcenter.heroku.com/articles/getting-started-with-rails7) -- [Buildpacks](https://devcenter.heroku.com/articles/buildpacks) -- [Buildpack API](https://devcenter.heroku.com/articles/buildpack-api) + ```sh + heroku config:set BUNDLE_GEMFILE=Gemfile.next -a + ``` -## Hacking +4. Deploy. This is the rebuild that installs the next gems (see the warning above): -To use this buildpack, fork it on Github. Push up changes to your fork, then create a test app with `--buildpack ` and push to it. + ```sh + git push heroku :main + ``` -### Testing +5. Verify: -```sh -$ bundle exec hatchet install -``` + ```sh + heroku run "bundle exec rails -v" -a + ``` + +To go back to the current version, unset the var and redeploy: ```sh -$ bundle exec rake spec +heroku config:unset BUNDLE_GEMFILE -a +# then deploy again ``` + +## Alternative: a next-only buildpack + +If you want an app that always runs the next version without managing `BUNDLE_GEMFILE`, use our sibling fork [`fastruby/heroku-buildpack-ruby-gemfile-next`](https://github.com/fastruby/heroku-buildpack-ruby-gemfile-next). It always uses `Gemfile.next` / `Gemfile.next.lock` and needs no `BUNDLE_GEMFILE`. To return to the current version, switch the app's buildpack back to the official `heroku/ruby`. + +## Documentation + +This fork only changes which Gemfile drives the build. For everything else, see: + +- [Official Heroku Ruby buildpack](https://github.com/heroku/heroku-buildpack-ruby) +- [Heroku Ruby Support](https://devcenter.heroku.com/articles/ruby-support) +- [Buildpacks](https://devcenter.heroku.com/articles/buildpacks) +- [next_rails](https://github.com/fastruby/next_rails) + +## License + +See [LICENSE](LICENSE). Originally created by Heroku, Inc. From 28e61bff61fe51903a84ddda6e7fec49d65ec0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 9 Jun 2026 13:34:24 -0600 Subject: [PATCH 10/12] Remove Check Changelog workflow 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. --- .github/workflows/check_changelog.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/workflows/check_changelog.yml diff --git a/.github/workflows/check_changelog.yml b/.github/workflows/check_changelog.yml deleted file mode 100644 index 968cc00aa..000000000 --- a/.github/workflows/check_changelog.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Check Changelog - -on: - pull_request: - types: [opened, reopened, labeled, unlabeled, synchronize] - -permissions: - contents: read - -jobs: - check-changelog: - runs-on: ubuntu-latest - if: (!contains(github.event.pull_request.labels.*.name, 'skip changelog')) - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Check that CHANGELOG is touched - run: | - git fetch origin ${{ github.base_ref }} --depth 1 && \ - git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md From da7f39262fbf85bb360de171301738dc1dcfadf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 9 Jun 2026 13:35:02 -0600 Subject: [PATCH 11/12] Remove remaining upstream-only workflows 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. --- .github/workflows/document_ruby_version.yml | 43 --------------------- .github/workflows/hatchet_app_cleaner.yml | 31 --------------- .github/workflows/prepare-release.yml | 27 ------------- 3 files changed, 101 deletions(-) delete mode 100644 .github/workflows/document_ruby_version.yml delete mode 100644 .github/workflows/hatchet_app_cleaner.yml delete mode 100644 .github/workflows/prepare-release.yml diff --git a/.github/workflows/document_ruby_version.yml b/.github/workflows/document_ruby_version.yml deleted file mode 100644 index 8eda9a650..000000000 --- a/.github/workflows/document_ruby_version.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Add Ruby version to changelog && prepare release -run-name: "Add ${{ inputs.is_jruby && 'J' || ''}}Ruby ${{ inputs.ruby_version }} to the CHANGELOG.md and prepare a release" - -on: - workflow_dispatch: - inputs: - ruby_version: - description: "The Ruby version to announce" - type: string - required: true - is_jruby: - description: "JRuby release? (as opposed to MRI)" - type: boolean - default: false - required: false - -# Disable all GITHUB_TOKEN permissions, since the GitHub App token is used instead. -permissions: {} - -jobs: - prepare-release: - uses: heroku/languages-github-actions/.github/workflows/_classic-buildpack-prepare-release.yml@latest - secrets: inherit - with: - custom_update_command: | - set -euo pipefail - DATE_TODAY="$(date --utc --iso-8601)" - - sed --in-place "/## \[v${NEW_VERSION}\] - ${DATE_TODAY}/a\\ - \\ - - ${{ inputs.is_jruby && 'J' || ''}}Ruby ${{inputs.ruby_version}} is now available" CHANGELOG.md - - sed --in-place --regexp-extended \ - --expression "s/v${EXISTING_VERSION}/v${NEW_VERSION}/" \ - lib/language_pack/version.rb - - if compgen -G 'changelogs/unreleased/*.md' > /dev/null; then - # The unreleased changelogs directory contains a `.gitkeep` file, so we have to - # copy the markdown files individually instead of renaming the directory. - NEW_CHANGELOG_DIR="changelogs/v${NEW_VERSION}/" - mkdir -p "${NEW_CHANGELOG_DIR}" - mv changelogs/unreleased/*.md "${NEW_CHANGELOG_DIR}" - fi diff --git a/.github/workflows/hatchet_app_cleaner.yml b/.github/workflows/hatchet_app_cleaner.yml deleted file mode 100644 index 28af64d7d..000000000 --- a/.github/workflows/hatchet_app_cleaner.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Hatchet app cleaner - -on: - schedule: - # Daily at 6am UTC. - - cron: "0 6 * * *" - # Allow the workflow to be manually triggered too. - workflow_dispatch: - -permissions: - contents: read - -jobs: - hatchet-app-cleaner: - runs-on: ubuntu-latest - env: - HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} - HEROKU_API_USER: ${{ secrets.HEROKU_API_USER }} - HEROKU_DISABLE_AUTOUPDATE: 1 - steps: - - name: Checkout - uses: actions/checkout@v6 - - name: Install Ruby and dependencies - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - ruby-version: "3.3.9" - - name: Run Hatchet destroy - # Only apps older than 10 minutes are destroyed, to ensure that any - # in progress CI runs are not interrupted. - run: bundle exec hatchet destroy --older-than 10 diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml deleted file mode 100644 index 2e6ff4c71..000000000 --- a/.github/workflows/prepare-release.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Prepare Release - -on: - workflow_dispatch: - -# Disable all GITHUB_TOKEN permissions, since the GitHub App token is used instead. -permissions: {} - -jobs: - prepare-release: - uses: heroku/languages-github-actions/.github/workflows/_classic-buildpack-prepare-release.yml@latest - secrets: inherit - with: - custom_update_command: | - set -euo pipefail - - sed --in-place --regexp-extended \ - --expression "s/v${EXISTING_VERSION}/v${NEW_VERSION}/" \ - lib/language_pack/version.rb - - if compgen -G 'changelogs/unreleased/*.md' > /dev/null; then - # The unreleased changelogs directory contains a `.gitkeep` file, so we have to - # copy the markdown files individually instead of renaming the directory. - NEW_CHANGELOG_DIR="changelogs/v${NEW_VERSION}/" - mkdir -p "${NEW_CHANGELOG_DIR}" - mv changelogs/unreleased/*.md "${NEW_CHANGELOG_DIR}" - fi From 709a88d788f581c5a6b4dce80b91f77e35ce2e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Fri, 12 Jun 2026 13:05:46 -0600 Subject: [PATCH 12/12] Update BUNDLE_GEMFILE re-release explanation in README Co-authored-by: arielj --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 5904aab2f..30baa56e0 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,7 @@ This is a fork of [Heroku's official Ruby buildpack](https://github.com/heroku/h > Example: an app built with Rails 6.1 only has 6.1 gems in its slug. If you then change `BUNDLE_GEMFILE` to `Gemfile.next` (Rails 7.0): > > - Heroku runs a new release with the changed env but does **not** rebuild the slug. -> - If you have a `release:` command in your `Procfile`, it runs against Rails 7.0, whose gems are not in the slug, so it **fails**. -> - 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. +> - The slug still has only the previous version's gems. In some cases the re-release may fail, and Heroku might revert the config var to its previous value. If that happens, the env change ends up **not actually applied at runtime**, so Heroku reports the var changed while the app keeps running the previous version. Confusing, but expected. > > **To switch versions: change `BUNDLE_GEMFILE` and then trigger a deploy (e.g. push a commit) so the correct gems are installed at build time.** Do not rely on flipping the config var by itself.