Skip to content

Commit eeb0516

Browse files
feat(bundler): enable Bundler 4 runtime support in helper flow (#14988)
* feat(bundler): enable Bundler 4 runtime support in helper flow * fix(bundler): address review feedback on version resolution - add upper bound (< 5) to prevent unintended Bundler 5+ activation - resolve Bundler version from GEM_HOME only, not all gem paths - add test specs for version resolution and override behavior * test(bundler): skip bundler<3 constraint specs under bundler 4 helper runtime * style(bundler): fix RuboCop RSpec offenses in v4 specs * fix(bundler): print resolved Bundler version in helper build script Without 'print', the Ruby one-liner produced an empty default_version, causing 'bundle __ install' to raise Illformed requirement [""]. * fix(common): allow Bundler 4 in dependabot-common gemspec * chore: update Gemfile.lock for bundler dependency bump * chore(updater): update Gemfile.lock for bundler dependency bump * fix(bundler): don't load run.rb in helper version spec run.rb has top-level code that reads from $stdin, which crashed the spec process and cascaded into all other helper specs. Replace the require with a direct check of Bundler::VERSION. * fix(bundler): support Bundler 4 in helper specs and APIs - Eagerly require RSpec built-in matchers in native_spec_helper to work around Bundler 4's stricter $LOAD_PATH that breaks lazy autoload. - Use safe matchers (include, be_positive) in bundler_version_spec. - Replace removed Bundler::SourceList#rubygems_remotes with rubygems_sources.flat_map(&:remotes), keeping a respond_to? fallback. - Replace removed Bundler::Index#search_all with #search, with fallback. - Split specified_source fixture lockfile into separate GEM sections per remote (Bundler 4 hard-removed multi-remote single source). * Address PR review feedback for Bundler 4 runtime support - run.rb now honours DEPENDABOT_BUNDLER_VERSION_CONSTRAINT and the BUNDLER_VERSION_CONSTRAINT fallback, so the activation constraint matches the version installed by build (and rollback works). - Extract BundlerVersionConstraint module and unit-test it in bundler_version_constraint_spec.rb (replacing the env-fetch round trip tests). Drop the dead 'not_to eq(3)' assertion. - Replace hard-coded /opt/bundler/v2/.../bundler-4.0.11.gemspec skip checks with PackageManagerHelper.helper_running_bundler_v4?, which inspects the helper's GEM_HOME so it works across patch bumps. - Restore bad-git-reference assertion in update_checker_spec by expecting GitDependencyReferenceNotFound on Bundler 2 and GitDependenciesNotReachable on Bundler 4. - Note in package_manager.rb that Bundler 3 was intentionally skipped upstream. - Document DEPENDABOT_BUNDLER_VERSION_CONSTRAINT and BUNDLER_VERSION_CONSTRAINT in bundler/README.md. * Tighten dependabot-common bundler lower bound to >= 2.4 Match the helper runtime, which already requires >= 2.4. Lockfiles are updated in lockstep.
1 parent 070d050 commit eeb0516

18 files changed

Lines changed: 218 additions & 22 deletions

File tree

Gemfile.lock

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ PATH
2929
dependabot-common (0.375.0)
3030
aws-sdk-codecommit (~> 1.28)
3131
aws-sdk-ecr (~> 1.5)
32-
bundler (>= 1.16, < 3.0.0)
32+
bundler (>= 2.4, < 5.0.0)
3333
commonmarker (~> 2.3)
3434
docker_registry2 (~> 1.18)
3535
excon (~> 1.2)
@@ -539,6 +539,7 @@ CHECKSUMS
539539
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
540540
benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
541541
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
542+
bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785
542543
citrus (3.0.2) sha256=4ec2412fc389ad186735f4baee1460f7900a8e130ffe3f216b30d4f9c684f650
543544
commonmarker (2.8.2-arm64-darwin) sha256=cd7364f1fb9735d60218e4af0a4afbbeedbc465eb81cccca56f29a4efe2b5013
544545
commonmarker (2.8.2-x86_64-linux) sha256=42cfb6532b15dd5821ce99e47397923255f32dcfc81024ef4c17e01e66ac7d75
@@ -674,4 +675,4 @@ CHECKSUMS
674675
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
675676

676677
BUNDLED WITH
677-
2.7.2
678+
4.0.11

bundler/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,18 @@ Ruby (bundler) support for [`dependabot-core`][core-repo].
1515
[dependabot-core-dev] ~ $ cd bundler && rspec
1616
```
1717

18+
### Native helper Bundler runtime
19+
20+
The native helper at `helpers/v2` runs under Bundler 4 by default. Two
21+
environment variables can override the Bundler version installed by `build`
22+
and activated by `run.rb`, which is intended for staged rollouts and
23+
emergency rollback to Bundler 2:
24+
25+
- `DEPENDABOT_BUNDLER_VERSION_CONSTRAINT` (preferred)
26+
- `BUNDLER_VERSION_CONSTRAINT` (fallback)
27+
28+
Both accept any RubyGems requirement string, e.g. `~> 4.0`, `~> 2.7`, or a
29+
comma-separated list like `>= 2.4, < 5`. When neither is set the helper uses
30+
`~> 4.0` for installation and `>= 2.4, < 5` for activation.
31+
1832
[core-repo]: https://github.com/dependabot/dependabot-core

bundler/helpers/v2/build

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,27 @@ fi
1919

2020
cd "$install_dir"
2121

22-
default_version=$(ruby -rbundler -e'print Bundler::VERSION')
22+
# Default to Bundler 4, with an override for controlled testing/rollouts.
23+
bundler_constraint="${DEPENDABOT_BUNDLER_VERSION_CONSTRAINT:-${BUNDLER_VERSION_CONSTRAINT:-~> 4.0}}"
2324

2425
export GEM_HOME=$install_dir/.bundle
2526

26-
gem install bundler -v "$default_version" --no-document
27+
gem install bundler -v "$bundler_constraint" --no-document
28+
29+
# Resolve the Bundler version that was actually installed in GEM_HOME to ensure
30+
# consistency with what was requested and to avoid picking up system gems.
31+
default_version=$(ruby -e '
32+
gemspecs = Dir.glob("#{ENV["GEM_HOME"]}/specifications/bundler-*.gemspec")
33+
latest = gemspecs.max_by { |f| Gem::Version.new(File.basename(f).match(/bundler-(.*)\.gemspec/)[1]) }
34+
abort("No bundler gemspec found in #{ENV["GEM_HOME"]}/specifications") unless latest
35+
print File.basename(latest).match(/bundler-(.*)\.gemspec/)[1]
36+
')
37+
38+
if [ -z "$default_version" ]; then
39+
echo "error: failed to resolve installed Bundler version in $GEM_HOME" >&2
40+
exit 1
41+
fi
2742

2843
if [ -z "$DEPENDABOT_NATIVE_HELPERS_PATH" ]; then
29-
bundle install
44+
bundle _"$default_version"_ install
3045
fi
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
# Resolves the Bundler version constraint that the native helper should use
5+
# at activation time. Honors DEPENDABOT_BUNDLER_VERSION_CONSTRAINT, falling
6+
# back to BUNDLER_VERSION_CONSTRAINT, and finally to the supplied default.
7+
#
8+
# Used by both `run.rb` (for activation via `gem`) and the helper specs so
9+
# the rollback/staged-rollout behavior is exercised by real code.
10+
module BundlerVersionConstraint
11+
DEFAULT_ACTIVATION_CONSTRAINT = ">= 2.4, < 5"
12+
13+
def self.resolve(env: ENV, default: DEFAULT_ACTIVATION_CONSTRAINT)
14+
env.fetch(
15+
"DEPENDABOT_BUNDLER_VERSION_CONSTRAINT",
16+
env.fetch("BUNDLER_VERSION_CONSTRAINT", default)
17+
)
18+
end
19+
20+
# Splits a comma-separated requirement string into the individual clauses
21+
# accepted by Kernel#gem (e.g. ">= 2.4, < 5" -> [">= 2.4", "< 5"]).
22+
def self.activation_clauses(constraint)
23+
constraint.split(",").map(&:strip)
24+
end
25+
end

bundler/helpers/v2/lib/functions.rb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,14 @@ def self.jfrog_source(**args)
9191
# Set flags and credentials
9292
set_bundler_flags_and_credentials(dir: args.fetch(:dir), credentials: args.fetch(:credentials))
9393

94-
Bundler::Definition.build(args.fetch(:gemfile_name), nil, {})
95-
.send(:sources)
96-
.rubygems_remotes
97-
.find { |uri| uri.host.include?("jfrog") }
98-
&.host
94+
sources = Bundler::Definition.build(args.fetch(:gemfile_name), nil, {}).send(:sources)
95+
# Bundler 4 removed SourceList#rubygems_remotes; use rubygems_sources + flat_map(&:remotes)
96+
remotes = if sources.respond_to?(:rubygems_remotes)
97+
sources.rubygems_remotes
98+
else
99+
sources.rubygems_sources.flat_map(&:remotes)
100+
end
101+
remotes.find { |uri| uri.host&.include?("jfrog") }&.host
99102
end
100103

101104
def self.git_specs(**args)

bundler/helpers/v2/lib/functions/dependency_source.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ def private_registry_versions
4141

4242
bundler_source
4343
.fetchers.flat_map do |fetcher|
44-
fetcher
45-
.specs([dependency_name], bundler_source)
46-
.search_all(dependency_name).map(&:version)
44+
index = fetcher.specs([dependency_name], bundler_source)
45+
# Bundler 4 removed Index#search_all; use #search which returns all matches
46+
specs = index.respond_to?(:search_all) ? index.search_all(dependency_name) : index.search(dependency_name)
47+
specs.map(&:version)
4748
end
4849
end
4950

bundler/helpers/v2/run.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
# typed: true
22
# frozen_string_literal: true
33

4-
gem "bundler", "~> 2.4"
4+
require_relative "lib/bundler_version_constraint"
5+
6+
# Allow Bundler 4 by default with an upper bound to prevent unintended future
7+
# major versions. Honor DEPENDABOT_BUNDLER_VERSION_CONSTRAINT (or its
8+
# BUNDLER_VERSION_CONSTRAINT fallback) so staged rollouts and emergency
9+
# rollbacks performed by the build script are respected at activation time.
10+
bundler_constraint = BundlerVersionConstraint.resolve
11+
gem "bundler", *BundlerVersionConstraint.activation_clauses(bundler_constraint)
512
require "bundler"
613
require "json"
714

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# typed: false
2+
# frozen_string_literal: true
3+
4+
require "native_spec_helper"
5+
require_relative "../lib/bundler_version_constraint"
6+
7+
RSpec.describe BundlerVersionConstraint do
8+
describe "helper runtime activation" do
9+
it "is running a supported Bundler major version" do
10+
# Bundler 3 was intentionally skipped upstream (Bundler jumped from 2.7
11+
# straight to 4.0 to align with RubyGems) so the supported window is
12+
# 2.x or 4.x.
13+
bundler_major = Bundler::VERSION.split(".").first.to_i
14+
expect(bundler_major).to be_between(2, 4)
15+
end
16+
end
17+
18+
describe ".resolve" do
19+
it "returns the DEPENDABOT_BUNDLER_VERSION_CONSTRAINT override when set" do
20+
env = { "DEPENDABOT_BUNDLER_VERSION_CONSTRAINT" => "~> 2.7" }
21+
expect(described_class.resolve(env: env)).to eq("~> 2.7")
22+
end
23+
24+
it "prefers DEPENDABOT_BUNDLER_VERSION_CONSTRAINT over BUNDLER_VERSION_CONSTRAINT" do
25+
env = {
26+
"DEPENDABOT_BUNDLER_VERSION_CONSTRAINT" => "~> 2.7",
27+
"BUNDLER_VERSION_CONSTRAINT" => "~> 4.0"
28+
}
29+
expect(described_class.resolve(env: env)).to eq("~> 2.7")
30+
end
31+
32+
it "falls back to BUNDLER_VERSION_CONSTRAINT when only that is set" do
33+
env = { "BUNDLER_VERSION_CONSTRAINT" => "~> 4.0" }
34+
expect(described_class.resolve(env: env)).to eq("~> 4.0")
35+
end
36+
37+
it "uses the default activation constraint when no env var is set" do
38+
expect(described_class.resolve(env: {})).to eq(">= 2.4, < 5")
39+
end
40+
41+
it "honours an explicit default override" do
42+
expect(described_class.resolve(env: {}, default: "~> 4.0")).to eq("~> 4.0")
43+
end
44+
end
45+
46+
describe ".activation_clauses" do
47+
it "splits comma-separated requirement strings into trimmed clauses" do
48+
expect(described_class.activation_clauses(">= 2.4, < 5")).to eq([">= 2.4", "< 5"])
49+
end
50+
51+
it "returns a single clause for a single requirement" do
52+
expect(described_class.activation_clauses("~> 4.0")).to eq(["~> 4.0"])
53+
end
54+
end
55+
56+
describe "build script GEM_HOME isolation" do
57+
it "resolves Bundler version from GEM_HOME only" do
58+
gem_home = ENV.fetch("GEM_HOME", nil)
59+
skip "GEM_HOME not set in test environment" unless gem_home
60+
61+
bundler_specs = Dir.glob("#{gem_home}/specifications/bundler-*.gemspec")
62+
expect(bundler_specs.length).to be_positive
63+
end
64+
end
65+
end

bundler/helpers/v2/spec/native_spec_helper.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
require "webmock/http_lib_adapters/excon_adapter"
77
require "debug"
88

9+
# Bundler 4's stricter $LOAD_PATH handling breaks RSpec's lazy autoload of
10+
# built-in matchers (e.g. `satisfy`, `raise_error`, `contain_exactly`, `has`).
11+
# Eagerly load all of them so tests don't hit LoadError mid-run.
12+
Gem.loaded_specs["rspec-expectations"]&.then do |spec|
13+
Dir[File.join(spec.full_gem_path, "lib/rspec/matchers/built_in/*.rb")].each { |f| require f }
14+
end
15+
916
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
1017
$LOAD_PATH.unshift(File.expand_path("../monkey_patches", __dir__))
1118
$LOAD_PATH.unshift(File.expand_path("../../spec_helpers", __dir__))

bundler/lib/dependabot/bundler/package_manager.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@ module Bundler
1111
ECOSYSTEM = "bundler"
1212
PACKAGE_MANAGER = "bundler"
1313

14-
# Keep versions in ascending order
15-
SUPPORTED_BUNDLER_VERSIONS = T.let([Version.new("2")].freeze, T::Array[Dependabot::Version])
14+
# Keep versions in ascending order.
15+
# Note: Bundler 3 was intentionally skipped upstream — Bundler jumped from
16+
# 2.7 directly to 4.0 to align its major version with RubyGems, so there
17+
# is no Bundler 3.x release to support.
18+
SUPPORTED_BUNDLER_VERSIONS = T.let(
19+
[Version.new("2"), Version.new("4")].freeze,
20+
T::Array[Dependabot::Version]
21+
)
1622

1723
# Currently, we don't support any deprecated versions of Bundler
1824
# When a version is going to be unsupported, it will be added here for a while to give users time to upgrade

0 commit comments

Comments
 (0)