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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 4351ae368..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,56 +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: - 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/ 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 diff --git a/README.md b/README.md index 3e4a55d1a..30baa56e0 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,79 @@ -# 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. +> - 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. -## 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. 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 0e4470a9e..59d9e84a2 100644 --- a/lib/language_pack.rb +++ b/lib/language_pack.rb @@ -9,14 +9,31 @@ 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 + if raw.empty? && defined?(LanguagePack::ShellHelpers) + raw = LanguagePack::ShellHelpers.user_env_hash["BUNDLE_GEMFILE"].to_s + end + 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.lock") + path = app_path.join(lockfile_name) if path.exist? LanguagePack::Helpers::GemfileLock.new( contents: path.read ) else - raise BuildpackError.new("Gemfile.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 386118518..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"), + 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 ab5452133..e314d5af8 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?(LanguagePack.gemfile_name) 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(LanguagePack.gemfile_name).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", LanguagePack.gemfile_name 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(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" @@ -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(LanguagePack.gemfile_name).to_s + base.merge(user_env_hash) end def database_url