From 1ac338aabf1bc974fd364b73545665f2d50dfb5d Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Mon, 14 Sep 2026 00:25:00 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Remove=20overrides=20of=20Bundle?= =?UTF-8?q?r's=20built-in=20git=20sources=20when=20templating=20Gemfiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundler already defines the github, gist and bitbucket git sources. A destination Gemfile override (for example an SSH git_source(:github) added by an earlier Gemfile template) replaces Bundler's own source and is recorded in Gemfile.lock, where CI cannot clone it. Removing the line from the packaged template does not fix already-templated projects, because the Gemfile merge keeps destination-only statements. Remove such overrides from merged Gemfile templates and from the managed monorepo root Gemfile. Other git sources (codeberg, gitlab) are not Bundler built-ins and are kept. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01P8aveNcdgPobyMrq8DkD9r --- gems/kettle-jem/CHANGELOG.md | 3 + gems/kettle-jem/lib/kettle/jem.rb | 22 +++++ .../appraisals_and_gemfile_template_spec.rb | 80 +++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/gems/kettle-jem/CHANGELOG.md b/gems/kettle-jem/CHANGELOG.md index df94b0716..3b0412fd0 100644 --- a/gems/kettle-jem/CHANGELOG.md +++ b/gems/kettle-jem/CHANGELOG.md @@ -42,6 +42,9 @@ Please file a bug if you notice a violation of semantic versioning. - The packaged `Gemfile` no longer overrides `git_source(:github)` with an SSH URL. The override was recorded in `Gemfile.lock` as `git@github.com` remotes, which CI runners cannot clone; Bundler's built-in HTTPS source is used again. + Templating now also removes existing overrides of Bundler's built-in git + sources (`git_source(:github)`, `:gist`, `:bitbucket`) from destination + Gemfiles, including the managed monorepo root Gemfile. - Generated Ruby 4 extracted-stdlib Gemfiles now include `ostruct` so suites that require it can run on Rubies where it is no longer bundled by default. diff --git a/gems/kettle-jem/lib/kettle/jem.rb b/gems/kettle-jem/lib/kettle/jem.rb index 212c54dc5..e64160e73 100644 --- a/gems/kettle-jem/lib/kettle/jem.rb +++ b/gems/kettle-jem/lib/kettle/jem.rb @@ -101,6 +101,10 @@ class Error < StandardError; end # turbo_tests were replaced by appraisal2 and turbo_tests2. PROHIBITED_GEMSPEC_DEPENDENCIES = %w[gem-release appraisal turbo_tests].freeze PROHIBITED_GEMFILE_DEPENDENCIES = PROHIBITED_GEMSPEC_DEPENDENCIES.freeze + # Git sources Bundler already defines. A Gemfile override (for example an + # SSH `git_source(:github)`) replaces Bundler's own HTTPS source and is + # recorded in Gemfile.lock, where CI cannot clone it: unconditional removal. + BUNDLER_BUILTIN_GIT_SOURCES = %w[github gist bitbucket].freeze # Canonical resolutions for development-dependency gems whose handling is # already known ahead of any specific destination project. Two # categories today: @@ -5530,6 +5534,7 @@ def ensure_monorepo_root_gemfile_dependencies(content) ensure_trailing_newline(content.to_s.empty? ? %(source "https://gem.coop"\n) : content.to_s).dup ) updated = remove_gemfile_dependency_blocks(updated, PROHIBITED_GEMFILE_DEPENDENCIES) + updated = remove_gemfile_builtin_git_source_overrides(updated).dup monorepo_root_gemfile_dependency_lines.each do |line| next if gemfile_declares_gem?(updated, line.fetch(:name)) @@ -8123,6 +8128,7 @@ def finalize_gemfile_template_source(recipe, content, destination_content, facts preserve_self_word_entries: local_gemfile_template_recipe?(recipe) ) end + output = remove_gemfile_builtin_git_source_overrides(output) if recipe.fetch(:target_path).to_s == "Gemfile" # A merged Gemfile retains its project-specific source; an accepted # template owns the complete source declaration. @@ -9700,6 +9706,22 @@ def remove_gemfile_dependency_blocks(content, gem_names) ensure_trailing_newline(lines.each_with_index.reject { |_line, index| remove_indexes.include?(index) }.map(&:first).join.gsub(/\n{3,}/, "\n\n")) end + # Bundler already provides these git sources; see BUNDLER_BUILTIN_GIT_SOURCES. + def remove_gemfile_builtin_git_source_overrides(content) + remove_indexes = Set.new + ruby_call_records(content, :git_source).each do |call| + argument = call.arguments&.arguments&.first + next unless argument.is_a?(::Prism::SymbolNode) + next unless BUNDLER_BUILTIN_GIT_SOURCES.include?(argument.unescaped.to_s) + + (call.location.start_line..ruby_node_source_end_line(call)).each { |line_number| remove_indexes << (line_number - 1) } + end + return content if remove_indexes.empty? + + lines = content.to_s.lines.each_with_index.reject { |_line, index| remove_indexes.include?(index) }.map(&:first) + ensure_trailing_newline(lines.join.sub(/\A\n+/, "").gsub(/\n{3,}/, "\n\n")) + end + def gemfile_gem_call_records(content) ruby_call_records(content, :gem).filter_map do |call| name = ruby_string_argument(call) diff --git a/gems/kettle-jem/spec/integration/appraisals_and_gemfile_template_spec.rb b/gems/kettle-jem/spec/integration/appraisals_and_gemfile_template_spec.rb index 2ed1230fa..d86e3d5a6 100644 --- a/gems/kettle-jem/spec/integration/appraisals_and_gemfile_template_spec.rb +++ b/gems/kettle-jem/spec/integration/appraisals_and_gemfile_template_spec.rb @@ -1450,6 +1450,86 @@ expect(updated).not_to include('gem "turbo_tests"') end + it "removes overrides of Bundler's built-in git sources and keeps other git sources" do + updated = described_class.remove_gemfile_builtin_git_source_overrides(<<~RUBY) + source "https://gem.coop" + + git_source(:github) { |repo_name| "git@github.com:\#{repo_name}.git" } + git_source(:gist) { |repo_name| "https://gist.github.com/\#{repo_name}.git" } + git_source(:bitbucket) do |repo_name| + "https://bitbucket.org/\#{repo_name}.git" + end + git_source(:codeberg) { |repo_name| "https://codeberg.org/\#{repo_name}" } + + gem "rubocop-packaging", github: "pboling/rubocop-packaging" + RUBY + + expect(updated).not_to include("git_source(:github)") + expect(updated).not_to include("git_source(:gist)") + expect(updated).not_to include("git_source(:bitbucket)") + expect(updated).not_to include("bitbucket.org") + expect(updated).to include("git_source(:codeberg)") + expect(updated).to include('gem "rubocop-packaging", github: "pboling/rubocop-packaging"') + end + + it "removes a git_source(:github) override from the managed root Gemfile" do + updated = described_class.ensure_monorepo_root_gemfile_dependencies( + "source \"https://gem.coop\"\ngit_source(:github) { |repo_name| \"https://github.com/\#{repo_name}\" }\n" + ) + + expect(updated).not_to include("git_source(:github)") + end + + it "removes a destination git_source(:github) override when templating the main Gemfile" do + tmp_root = File.expand_path("../tmp", __dir__) + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-main-gemfile-builtin-git-source", tmp_root) do |root| + write_tree(root, { + "example-gem.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example-gem" + spec.summary = "Example Gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + templates: + root: template + apply: true + entries: + - Gemfile + YAML + "Gemfile" => <<~RUBY, + # frozen_string_literal: true + + source "https://gem.coop" + + git_source(:github) { |repo_name| "git@github.com:\#{repo_name}.git" } + git_source(:codeberg) { |repo_name| "https://codeberg.org/\#{repo_name}" } + + gem "destination-only" + RUBY + "template/Gemfile.example" => <<~RUBY + # frozen_string_literal: true + + source "https://gem.coop" + + git_source(:codeberg) { |repo_name| "https://codeberg.org/\#{repo_name}" } + + gem "shared-tool" + RUBY + }) + + apply = described_class.apply_project(root, env: {}) + report = apply.fetch(:recipe_reports).find { |candidate| candidate.fetch(:relative_path) == "Gemfile" } + content = report.fetch(:final_content) + + expect(content).not_to include("git_source(:github)") + expect(content).to include("git_source(:codeberg)") + expect(content).to include('gem "shared-tool"') + expect(File.read(File.join(root, "Gemfile"))).to eq(content) + end + end + it "replaces declared kettle-dev dependencies with local paths without duplicate declarations" do template = File.read(File.expand_path("../../lib/kettle/jem/templates/gemfiles/modular/templating_local.gemfile.example", __dir__)) coverage_template = File.read(File.expand_path("../../lib/kettle/jem/templates/gemfiles/modular/coverage_local.gemfile.example", __dir__))