From 9b2514889fec59bb3b97a7af8b3f4391eb11c5f6 Mon Sep 17 00:00:00 2001 From: Patrick Linnane Date: Fri, 22 May 2026 19:07:18 -0700 Subject: [PATCH 1/2] test-bot: inline portable-ruby validation into `portable_formula!` After `brew bottle portable-ruby` succeeds, stage the just-built bottle into `HOMEBREW_CACHE`, swap in the vendored portable-ruby, and run the Homebrew/brew PR CI surface against it so issues are caught in the homebrew-core PR rather than the follow-up brew bump PR. Shared bundler-syncing logic lives in a small `Utils::PortableRuby` helper for reuse with the new `bump-portable-ruby` workflow. --- Library/Homebrew/test_bot.rb | 1 + Library/Homebrew/test_bot/formulae.rb | 66 +++++++++++++++++++++++-- Library/Homebrew/utils/portable_ruby.rb | 29 +++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 Library/Homebrew/utils/portable_ruby.rb diff --git a/Library/Homebrew/test_bot.rb b/Library/Homebrew/test_bot.rb index 3a0fdfd67a6f9..c5c6d12d9a09a 100644 --- a/Library/Homebrew/test_bot.rb +++ b/Library/Homebrew/test_bot.rb @@ -15,6 +15,7 @@ require "tap" require "utils" require "utils/bottles" +require "utils/portable_ruby" module Homebrew module TestBot diff --git a/Library/Homebrew/test_bot/formulae.rb b/Library/Homebrew/test_bot/formulae.rb index d7d83cfbb5099..37b4a6648a512 100644 --- a/Library/Homebrew/test_bot/formulae.rb +++ b/Library/Homebrew/test_bot/formulae.rb @@ -69,7 +69,7 @@ def run!(args:) sorted_formulae.each do |f| verify_local_bottles if testing_portable_ruby? - portable_formula!(f) + portable_formula!(f, args:) else formula!(f, args:) end @@ -754,8 +754,8 @@ def formula!(formula_name, args:) end end - sig { params(formula_name: String).void } - def portable_formula!(formula_name) + sig { params(formula_name: String, args: Homebrew::Cmd::TestBotCmd::Args).void } + def portable_formula!(formula_name, args:) test_header(:Formulae, method: "portable_formula!(#{formula_name})") install_ca_certificates_if_needed @@ -788,6 +788,66 @@ def portable_formula!(formula_name) test "brew", "test", formula_name test "brew", "linkage", formula_name test "brew", "bottle", "--skip-relocation", "--json", "--no-rebuild", formula_name + + # We only do full testing on `portable-ruby` itself. + return if formula_name != "portable-ruby" + return if args.dry_run? + + bottle_file = bottle_glob(formula_name).first + if bottle_file.nil? + failed formula_name, "no bottle file found for portable-ruby validation" + return + end + + filename = bottle_file.basename.to_s + _, tag_string, = Utils::Bottles.extname_tag_rebuild(filename) + if tag_string.blank? + failed formula_name, "could not parse bottle filename #{filename}" + return + end + + pkg_version = filename.delete_prefix("portable-ruby--") + .sub(/\.#{Regexp.escape(tag_string)}\.bottle.*\.tar\.gz\z/, "") + if pkg_version.empty? + failed formula_name, "could not parse portable-ruby version from #{filename}" + return + end + + tag_symbol = tag_string.to_sym + bottle_tag = Utils::Bottles::Tag.from_symbol(tag_symbol) + sha256 = bottle_file.sha256 + version = pkg_version.split("_").first.to_s + + vendor_dir = HOMEBREW_LIBRARY_PATH/"vendor" + (vendor_dir/"portable-ruby-version").atomic_write("#{pkg_version}\n") + (HOMEBREW_LIBRARY_PATH/".ruby-version").atomic_write("#{version}\n") + os = bottle_tag.linux? ? "linux" : "darwin" + platform_file = vendor_dir/"portable-ruby-#{bottle_tag.standardized_arch}-#{os}" + platform_file.atomic_write("ruby_TAG=#{tag_symbol}\nruby_SHA=#{sha256}\n") + + # Seed `HOMEBREW_CACHE` so `brew vendor-install ruby` finds the just-built + # bottle locally instead of trying to download it. + HOMEBREW_CACHE.mkpath + FileUtils.cp(bottle_file, HOMEBREW_CACHE/"portable-ruby-#{pkg_version}.#{tag_symbol}.bottle.tar.gz") + + test "brew", "vendor-install", "ruby" + + bundler_version = Utils::PortableRuby.sync_bundler_version!(pkg_version) + test "brew", "vendor-gems", "--no-commit", "--update=--ruby,--bundler=#{bundler_version}" + test "brew", "typecheck", "--update" + + # Run the checks that gate a Homebrew/brew pull request. + test "brew", "style" + test "brew", "typecheck" + test "brew", "install-bundler-gems", "--groups=all" + test "brew", "vendor-gems", "--non-bundler-gems", "--no-commit" + test "brew", "tests", "--online", "--coverage" + test "brew", "tests", "--generic", "--coverage" + test "brew", "update-test" + test "brew", "update-test", "--to-tag" + test "brew", "update-test", "--commit=HEAD" + test "brew", "test-bot", "--only-formulae", "--only-json-tab", "--test-default-formula", + env: { "GITHUB_ACTIONS" => nil } end sig { params(formula_name: String).void } diff --git a/Library/Homebrew/utils/portable_ruby.rb b/Library/Homebrew/utils/portable_ruby.rb new file mode 100644 index 0000000000000..9b5642a3d4eb4 --- /dev/null +++ b/Library/Homebrew/utils/portable_ruby.rb @@ -0,0 +1,29 @@ +# typed: strict +# frozen_string_literal: true + +require "utils/output" + +module Utils + # Helper functions for the vendored portable-ruby. + module PortableRuby + extend Utils::Output::Mixin + + # Syncs `HOMEBREW_BUNDLER_VERSION` in `utils/ruby.sh` with the bundler shipped + # by the portable-ruby unpacked at `pkg_version`. + sig { params(pkg_version: String).returns(String) } + def self.sync_bundler_version!(pkg_version) + unpacked = HOMEBREW_LIBRARY_PATH/"vendor/portable-ruby/#{pkg_version}" + bundler_dir = Pathname.glob(unpacked/"lib/ruby/gems/*/gems/bundler-*").first + odie "Cannot find vendored bundler for portable-ruby #{pkg_version}." if bundler_dir.nil? + + bundler_version = bundler_dir.basename.to_s.delete_prefix("bundler-") + + ruby_sh = HOMEBREW_LIBRARY_PATH/"utils/ruby.sh" + original = ruby_sh.read + updated = original.sub(/(?<=^export HOMEBREW_BUNDLER_VERSION=")[^"]+/, bundler_version) + ruby_sh.atomic_write(updated) if original != updated + + bundler_version + end + end +end From ba52eced93d32ac5110a0aec694f09b015343a3e Mon Sep 17 00:00:00 2001 From: Patrick Linnane Date: Sat, 23 May 2026 15:03:44 -0700 Subject: [PATCH 2/2] update-portable-ruby: hide and consolidate into one-shot Rewrites `update-portable-ruby` as a single hidden command that reads the `portable-ruby` formula, writes vendor files, runs `brew vendor-install ruby`, then syncs `utils/ruby.sh`, vendored gems and RBI files via the new `Utils::PortableRuby.sync_bundler_version!` helper. Drops `--dry-run` and `--skip-vendor-install` and hides from `brew help` since this is now meant to be invoked from the homebrew-core bottle-publish workflow rather than by hand. Also scopes the `actions/create-github-app-token` permissions in `vendor-gems.yml` to `contents: write`. --- .github/workflows/vendor-gems.yml | 1 + .../Homebrew/dev-cmd/update-portable-ruby.rb | 66 ++++--------------- .../homebrew/dev_cmd/update_portable_ruby.rbi | 11 +--- 3 files changed, 15 insertions(+), 63 deletions(-) diff --git a/.github/workflows/vendor-gems.yml b/.github/workflows/vendor-gems.yml index d19dc911e78f8..69755677335ff 100644 --- a/.github/workflows/vendor-gems.yml +++ b/.github/workflows/vendor-gems.yml @@ -128,6 +128,7 @@ jobs: with: app-id: ${{ vars.BREW_COMMIT_APP_ID }} private-key: ${{ secrets.BREW_COMMIT_APP_KEY }} + permission-contents: write - name: Push to pull request if: github.event_name == 'workflow_dispatch' diff --git a/Library/Homebrew/dev-cmd/update-portable-ruby.rb b/Library/Homebrew/dev-cmd/update-portable-ruby.rb index f895f6ecd3302..cc00107ccd032 100644 --- a/Library/Homebrew/dev-cmd/update-portable-ruby.rb +++ b/Library/Homebrew/dev-cmd/update-portable-ruby.rb @@ -4,88 +4,48 @@ require "abstract_command" require "formula" require "utils/bottles" +require "utils/portable_ruby" module Homebrew module DevCmd class UpdatePortableRuby < AbstractCommand cmd_args do description <<~EOS - Update the vendored portable Ruby version files, bottle checksums, - `utils/ruby.sh` and `Gemfile.lock` entries from the current - `portable-ruby` formula. + Update the vendored `portable-ruby` from the current `portable-ruby` formula: + write the version files and bottle checksums, run `brew vendor-install ruby`, + then sync `utils/ruby.sh`, vendored gems and RBI files to the bundler shipped + by the new ruby. EOS - switch "-n", "--dry-run", - description: "Print what would be done rather than doing it." - switch "--skip-vendor-install", - description: "Do not run `brew vendor-install ruby`; skip the `utils/ruby.sh`, " \ - "`Gemfile.lock` and RBI updates." - named_args :none + + hide_from_man_page! end sig { override.void } def run formula = Homebrew.with_no_api_env { Formulary.factory("portable-ruby") } - version = formula.version.to_s pkg_version = formula.pkg_version.to_s vendor_dir = HOMEBREW_LIBRARY_PATH/"vendor" - write_file(vendor_dir/"portable-ruby-version", "#{pkg_version}\n") - write_file(HOMEBREW_LIBRARY_PATH/".ruby-version", "#{version}\n") + (vendor_dir/"portable-ruby-version").atomic_write("#{pkg_version}\n") + (HOMEBREW_LIBRARY_PATH/".ruby-version").atomic_write("#{version}\n") formula.bottle_specification.checksums.each do |checksum| tag_symbol = checksum.fetch("tag") tag = Utils::Bottles::Tag.from_symbol(tag_symbol) os = tag.linux? ? "linux" : "darwin" path = vendor_dir/"portable-ruby-#{tag.standardized_arch}-#{os}" - write_file(path, "ruby_TAG=#{tag_symbol}\nruby_SHA=#{checksum.fetch("digest")}\n") + path.atomic_write("ruby_TAG=#{tag_symbol}\nruby_SHA=#{checksum.fetch("digest")}\n") end - return if args.skip_vendor_install? - - if args.dry_run? - ohai "brew vendor-install ruby" - ohai "Would update #{HOMEBREW_LIBRARY_PATH/"utils/ruby.sh"} and #{HOMEBREW_LIBRARY_PATH/"Gemfile.lock"} " \ - "with the bundler version shipped by portable-ruby #{pkg_version}." - ohai "brew typecheck --update" - return - end - - ohai "brew vendor-install ruby" safe_system HOMEBREW_BREW_FILE, "vendor-install", "ruby" - bundler_dir = Pathname.glob(vendor_dir/"portable-ruby/#{pkg_version}/lib/ruby/gems/*/gems/bundler-*").first - odie "Cannot find vendored bundler for portable-ruby #{pkg_version}." if bundler_dir.nil? - bundler_version = bundler_dir.basename.to_s.delete_prefix("bundler-") - - ruby_sh = HOMEBREW_LIBRARY_PATH/"utils/ruby.sh" - original = ruby_sh.read - updated = original.sub(/(?<=^export HOMEBREW_BUNDLER_VERSION=")[^"]+/, bundler_version) - if original != updated - ohai "Writing #{ruby_sh}" - ruby_sh.atomic_write(updated) - end - - ohai "brew vendor-gems --no-commit --update=--ruby,--bundler=#{bundler_version}" - safe_system HOMEBREW_BREW_FILE, "vendor-gems", "--no-commit", "--update=--ruby,--bundler=#{bundler_version}" - - ohai "brew typecheck --update" + bundler_version = Utils::PortableRuby.sync_bundler_version!(pkg_version) + safe_system HOMEBREW_BREW_FILE, "vendor-gems", "--no-commit", + "--update=--ruby,--bundler=#{bundler_version}" safe_system HOMEBREW_BREW_FILE, "typecheck", "--update" end - - private - - sig { params(path: Pathname, contents: String).void } - def write_file(path, contents) - if args.dry_run? - ohai "Write #{path}:" - puts contents - else - ohai "Writing #{path}" - path.atomic_write(contents) - end - end end end end diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/update_portable_ruby.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/update_portable_ruby.rbi index 4cc87ab2b545a..ee7540f4be874 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/update_portable_ruby.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/dev_cmd/update_portable_ruby.rbi @@ -10,13 +10,4 @@ class Homebrew::DevCmd::UpdatePortableRuby def args; end end -class Homebrew::DevCmd::UpdatePortableRuby::Args < Homebrew::CLI::Args - sig { returns(T::Boolean) } - def dry_run?; end - - sig { returns(T::Boolean) } - def n?; end - - sig { returns(T::Boolean) } - def skip_vendor_install?; end -end +class Homebrew::DevCmd::UpdatePortableRuby::Args < Homebrew::CLI::Args; end