diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67ea2da218..c210fece8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,24 +16,25 @@ jobs: fail-fast: false matrix: suite: - - bundler - - cargo - - common - - composer - - dep - - docker - - elm - - git_submodules - - github_actions - - go_modules - - gradle - - hex - - maven - - npm_and_yarn - - nuget - - omnibus - - python - - terraform + - { path: bundler, name: bundler1 } + - { path: bundler, name: bundler2 } + - { path: cargo, name: cargo } + - { path: common, name: common } + - { path: composer, name: composer } + - { path: dep, name: dep } + - { path: docker, name: docker } + - { path: elm, name: elm } + - { path: git_submodules, name: git_submodules } + - { path: github_actions, name: github_actions } + - { path: go_modules, name: go_modules } + - { path: gradle, name: gradle } + - { path: hex, name: hex } + - { path: maven, name: maven } + - { path: npm_and_yarn, name: npm_and_yarn } + - { path: nuget, name: nuget } + - { path: omnibus, name: omnibus } + - { path: python, name: python } + - { path: terraform, name: terraform } steps: - name: Checkout code uses: actions/checkout@v2 @@ -91,28 +92,29 @@ jobs: docker push "$CORE_CI_IMAGE:latest" docker push "$CORE_CI_IMAGE:ci--$BRANCH_REF" - name: Run Python flake8 linting - if: matrix.suite == 'python' + if: matrix.suite.name == 'python' run: | docker run --rm "$CORE_CI_IMAGE" bash -c "pyenv exec flake8 python/helpers/. --count --exclude=./.*,./python/spec/fixtures --show-source --statistics" - name: Run Ruby Rubocop linting run: | - docker run --rm "$CORE_CI_IMAGE" bash -c "cd /home/dependabot/dependabot-core/${{ matrix.suite }} && bundle exec rubocop ." + docker run --rm "$CORE_CI_IMAGE" bash -c "cd /home/dependabot/dependabot-core/${{ matrix.suite.path }} && bundle exec rubocop ." - name: Run js linting and tests - if: matrix.suite == 'npm_and_yarn' + if: matrix.suite.name == 'npm_and_yarn' run: | docker run --rm "$CORE_CI_IMAGE" bash -c "cd /opt/npm_and_yarn && npm run lint" docker run --rm "$CORE_CI_IMAGE" bash -c "cd /opt/npm_and_yarn && npm test" - name: Run bundler v1 native helper specs - if: matrix.suite == 'bundler' + if: matrix.suite.name == 'bundler1' run: | docker run --rm "$CORE_CI_IMAGE" bash -c \ "cd /home/dependabot/dependabot-core/bundler/helpers/v1 && BUNDLER_VERSION=1 bundle install && BUNDLER_VERSION=1 bundle exec rspec spec" - name: Run bundler v2 native helper specs - if: matrix.suite == 'bundler' + if: matrix.suite.name == 'bundler2' run: | docker run --rm "$CORE_CI_IMAGE" bash -c \ "cd /home/dependabot/dependabot-core/bundler/helpers/v2 && BUNDLER_VERSION=2 bundle install && BUNDLER_VERSION=2 bundle exec rspec spec" - - name: Run ${{ matrix.suite }} tests with rspec + - name: Run ${{ matrix.suite.name }} tests with rspec run: | - docker run --env "CI=true" --env "DEPENDABOT_TEST_ACCESS_TOKEN=$DEPENDABOT_TEST_ACCESS_TOKEN" --rm "$CORE_CI_IMAGE" bash -c \ - "cd /home/dependabot/dependabot-core/${{ matrix.suite }} && bundle exec rspec spec" + echo "SUITE_NAME=${{ matrix.suite.name }}" >> $GITHUB_ENV + docker run --env "CI=true" --env "DEPENDABOT_TEST_ACCESS_TOKEN=$DEPENDABOT_TEST_ACCESS_TOKEN" --env "SUITE_NAME=$SUITE_NAME" --rm "$CORE_CI_IMAGE" bash -c \ + "cd /home/dependabot/dependabot-core/${{ matrix.suite.path }} && bundle exec rspec spec" diff --git a/bundler/helpers/v2/.bundle/config b/bundler/helpers/v2/.bundle/config deleted file mode 100644 index 04f57bd2ed..0000000000 --- a/bundler/helpers/v2/.bundle/config +++ /dev/null @@ -1,2 +0,0 @@ ---- -BUNDLE_PATH: ".bundle" diff --git a/bundler/helpers/v2/.gitignore b/bundler/helpers/v2/.gitignore index 9e978b139f..c88c5c2435 100644 --- a/bundler/helpers/v2/.gitignore +++ b/bundler/helpers/v2/.gitignore @@ -1,5 +1,4 @@ -/.bundle/* -!/.bundle/config +/.bundle /.env /tmp /dependabot-*.gem diff --git a/bundler/helpers/v2/build b/bundler/helpers/v2/build index 104852b802..b015cacdd1 100755 --- a/bundler/helpers/v2/build +++ b/bundler/helpers/v2/build @@ -10,7 +10,6 @@ fi helpers_dir="$(dirname "${BASH_SOURCE[0]}")" cp -r \ - "$helpers_dir/.bundle" \ "$helpers_dir/lib" \ "$helpers_dir/run.rb" \ "$helpers_dir/Gemfile" \ @@ -20,4 +19,5 @@ cd "$install_dir" # NOTE: Sets `BUNDLED WITH` to match the installed v1 version in Gemfile.lock # forcing specs and native helpers to run with the same version -BUNDLER_VERSION=2 bundle install +BUNDLER_VERSION=2 bundle config set --local path ".bundle" +BUNDLER_VERSION=2 bundle install --without test diff --git a/bundler/helpers/v2/lib/functions.rb b/bundler/helpers/v2/lib/functions.rb index 9b52d95d00..22c26fb9f8 100644 --- a/bundler/helpers/v2/lib/functions.rb +++ b/bundler/helpers/v2/lib/functions.rb @@ -1,12 +1,20 @@ +require "functions/file_parser" + module Functions class NotImplementedError < StandardError; end def self.parsed_gemfile(lockfile_name:, gemfile_name:, dir:) - raise NotImplementedError, "Bundler 2 adapter does not yet implement #{__method__}" + set_bundler_flags_and_credentials(dir: dir, credentials: [], + using_bundler2: false) + FileParser.new(lockfile_name: lockfile_name). + parsed_gemfile(gemfile_name: gemfile_name) end def self.parsed_gemspec(lockfile_name:, gemspec_name:, dir:) - raise NotImplementedError, "Bundler 2 adapter does not yet implement #{__method__}" + set_bundler_flags_and_credentials(dir: dir, credentials: [], + using_bundler2: false) + FileParser.new(lockfile_name: lockfile_name). + parsed_gemspec(gemspec_name: gemspec_name) end def self.vendor_cache_dir(dir:) @@ -57,7 +65,47 @@ def self.git_specs(dir:, gemfile_name:, credentials:, using_bundler2:) def self.set_bundler_flags_and_credentials(dir:, credentials:, using_bundler2:) - raise NotImplementedError, "Bundler 2 adapter does not yet implement #{__method__}" + dir = dir ? Pathname.new(dir) : dir + Bundler.instance_variable_set(:@root, dir) + + # Remove installed gems from the default Rubygems index + Gem::Specification.all = + Gem::Specification.send(:default_stubs, "*.gemspec") + + # Set auth details + relevant_credentials(credentials).each do |cred| + token = cred["token"] || + "#{cred['username']}:#{cred['password']}" + + Bundler.settings.set_command_option( + cred.fetch("host"), + token.gsub("@", "%40F").gsub("?", "%3F") + ) + end + + # NOTE: Prevent bundler from printing resolution information + Bundler.ui = Bundler::UI::Silent.new + + # Use HTTPS for GitHub if lockfile + Bundler.settings.set_command_option("forget_cli_options", "true") + Bundler.settings.set_command_option("github.https", "true") + end + + def self.relevant_credentials(credentials) + [ + *git_source_credentials(credentials), + *private_registry_credentials(credentials) + ].select { |cred| cred["password"] || cred["token"] } + end + + def self.private_registry_credentials(credentials) + credentials. + select { |cred| cred["type"] == "rubygems_server" } + end + + def self.git_source_credentials(credentials) + credentials. + select { |cred| cred["type"] == "git_source" } end def self.conflicting_dependencies(dir:, dependency_name:, target_version:, diff --git a/bundler/helpers/v2/lib/functions/file_parser.rb b/bundler/helpers/v2/lib/functions/file_parser.rb new file mode 100644 index 0000000000..0066abce19 --- /dev/null +++ b/bundler/helpers/v2/lib/functions/file_parser.rb @@ -0,0 +1,106 @@ +module Functions + class FileParser + def initialize(lockfile_name:) + @lockfile_name = lockfile_name + end + + attr_reader :lockfile_name + + def parsed_gemfile(gemfile_name:) + Bundler::Definition.build(gemfile_name, nil, {}). + dependencies.select(&:current_platform?). + reject { |dep| dep.source.is_a?(Bundler::Source::Gemspec) }. + map(&method(:serialize_bundler_dependency)) + end + + def parsed_gemspec(gemspec_name:) + Bundler.load_gemspec_uncached(gemspec_name). + dependencies. + map(&method(:serialize_bundler_dependency)) + end + + private + + def lockfile + return @lockfile if defined?(@lockfile) + + @lockfile = + begin + return unless lockfile_name && File.exist?(lockfile_name) + + File.read(lockfile_name) + end + end + + def parsed_lockfile + return unless lockfile + + @parsed_lockfile ||= Bundler::LockfileParser.new(lockfile) + end + + def source_from_lockfile(dependency_name) + parsed_lockfile&.specs.find { |s| s.name == dependency_name }&.source + end + + def source_for(dependency) + source = dependency.source + if lockfile && default_rubygems?(source) + # If there's a lockfile and the Gemfile doesn't have anything + # interesting to say about the source, check that. + source = source_from_lockfile(dependency.name) + end + raise "Bad source: #{source}" unless sources.include?(source.class) + + return nil if default_rubygems?(source) + + details = { type: source.class.name.split("::").last.downcase } + if source.is_a?(Bundler::Source::Git) + details.merge!(git_source_details(source)) + end + if source.is_a?(Bundler::Source::Rubygems) + details[:url] = source.remotes.first.to_s + end + details + end + + # TODO: Remove default `master` branch + def git_source_details(source) + { + url: source.uri, + branch: source.branch || "master", + ref: source.ref || "master" + } + end + + def default_rubygems?(source) + return true if source.nil? + return false unless source.is_a?(Bundler::Source::Rubygems) + + source.remotes.any? { |r| r.to_s.include?("rubygems.org") } + end + + def serialize_bundler_dependency(dependency) + { + name: dependency.name, + requirement: dependency.requirement, + groups: dependency.groups, + source: source_for(dependency), + type: dependency.type + } + end + + # Can't be a constant because some of these don't exist in bundler + # 1.15, which used to cause issues on Heroku (causing exception on boot). + # TODO: Check if this will be an issue with multiple bundler versions + def sources + [ + NilClass, + Bundler::Source::Rubygems, + Bundler::Source::Git, + Bundler::Source::Path, + Bundler::Source::Gemspec, + Bundler::Source::Metadata + ] + end + end +end diff --git a/bundler/helpers/v2/monkey_patches/definition_bundler_version_patch.rb b/bundler/helpers/v2/monkey_patches/definition_bundler_version_patch.rb new file mode 100644 index 0000000000..6734d4c8e1 --- /dev/null +++ b/bundler/helpers/v2/monkey_patches/definition_bundler_version_patch.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "bundler/definition" + +# Ignore the Bundler version specified in the Gemfile (since the only Bundler +# version available to us is the one we're using). +module BundlerDefinitionBundlerVersionPatch + def expanded_dependencies + @expanded_dependencies ||= + expand_dependencies(dependencies + metadata_dependencies, @remote). + reject { |d| d.name == "bundler" } + end +end + +Bundler::Definition.prepend(BundlerDefinitionBundlerVersionPatch) diff --git a/bundler/helpers/v2/monkey_patches/definition_ruby_version_patch.rb b/bundler/helpers/v2/monkey_patches/definition_ruby_version_patch.rb new file mode 100644 index 0000000000..f33351e643 --- /dev/null +++ b/bundler/helpers/v2/monkey_patches/definition_ruby_version_patch.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "bundler/definition" + +module BundlerDefinitionRubyVersionPatch + def index + @index ||= super.tap do + if ruby_version + requested_version = ruby_version.to_gem_version_with_patchlevel + sources.metadata_source.specs << + Gem::Specification.new("ruby\0", requested_version) + end + + sources.metadata_source.specs << + Gem::Specification.new("ruby\0", "2.5.3p105") + end + end +end + +Bundler::Definition.prepend(BundlerDefinitionRubyVersionPatch) diff --git a/bundler/helpers/v2/monkey_patches/git_source_patch.rb b/bundler/helpers/v2/monkey_patches/git_source_patch.rb new file mode 100644 index 0000000000..13c3feb99f --- /dev/null +++ b/bundler/helpers/v2/monkey_patches/git_source_patch.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "bundler/source" + +module Bundler + class Source + class Git + class GitProxy + private + + # Bundler allows ssh authentication when talking to GitHub but there's + # no way for Dependabot to do so (it doesn't have any ssh keys). + # Instead, we convert all `git@github.com:` URLs to use HTTPS. + def configured_uri_for(uri) + uri = uri.gsub(%r{git@(.*?):/?}, 'https://\1/') + if /https?:/ =~ uri + remote = Bundler::URI(uri) + config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host] + remote.userinfo ||= config_auth + remote.to_s + else + uri + end + end + end + end + end +end + +module Bundler + class Source + class Git < Path + private + + def serialize_gemspecs_in(destination) + original_load_paths = $LOAD_PATH.dup + reduced_load_paths = original_load_paths. + reject { |p| p.include?("/gems/") } + + $LOAD_PATH.shift until $LOAD_PATH.empty? + reduced_load_paths.each { |p| $LOAD_PATH << p } + + if destination.relative? + destination = destination.expand_path(Bundler.root) + end + Dir["#{destination}/#{@glob}"].each do |spec_path| + # Evaluate gemspecs and cache the result. Gemspecs + # in git might require git or other dependencies. + # The gemspecs we cache should already be evaluated. + spec = Bundler.load_gemspec(spec_path) + next unless spec + + Bundler.rubygems.set_installed_by_version(spec) + Bundler.rubygems.validate(spec) + File.open(spec_path, "wb") { |file| file.write(spec.to_ruby) } + end + $LOAD_PATH.shift until $LOAD_PATH.empty? + original_load_paths.each { |p| $LOAD_PATH << p } + end + end + end +end diff --git a/bundler/helpers/v2/spec/functions/file_parser_spec.rb b/bundler/helpers/v2/spec/functions/file_parser_spec.rb new file mode 100644 index 0000000000..7b5b186d62 --- /dev/null +++ b/bundler/helpers/v2/spec/functions/file_parser_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require "native_spec_helper" +require "shared_contexts" + +RSpec.describe Functions::FileParser do + include_context "in a temporary bundler directory" + + let(:dependency_source) do + described_class.new( + lockfile_name: "Gemfile.lock" + ) + end + + let(:project_name) { "gemfile" } + + describe "#parsed_gemfile" do + subject(:parsed_gemfile) do + in_tmp_folder do + dependency_source.parsed_gemfile(gemfile_name: "Gemfile") + end + end + + it "parses gemfile" do + parsed_gemfile = [ + { + groups: [:default], + name: "business", + requirement: Gem::Requirement.new("~> 1.4.0"), + source: nil, + type: :runtime + }, + { + groups: [:default], + name: "statesman", + requirement: Gem::Requirement.new("~> 1.2.0"), + source: nil, + type: :runtime + } + ] + is_expected.to eq(parsed_gemfile) + end + + context "with a git source" do + let(:project_name) { "git_source" } + + it "parses gemfile" do + parsed_gemfile = [ + { + groups: [:default], + name: "business", + requirement: Gem::Requirement.new("~> 1.6.0"), + source: { + branch: "master", + ref: "a1b78a9", + type: "git", + url: "git@github.com:gocardless/business" + }, + type: :runtime + }, + { + groups: [:default], + name: "statesman", + requirement: Gem::Requirement.new("~> 1.2.0"), + source: nil, + type: :runtime + }, + { + groups: [:default], + name: "prius", + requirement: Gem::Requirement.new(">= 0"), + source: { + branch: "master", + ref: "master", + type: "git", + url: "https://github.com/gocardless/prius" + }, + type: :runtime + }, + { + groups: [:default], + name: "que", + requirement: Gem::Requirement.new(">= 0"), + source: { + branch: "master", + ref: "v0.11.6", + type: "git", + url: "git@github.com:chanks/que" + }, + type: :runtime + }, + { + groups: [:default], + name: "uk_phone_numbers", + requirement: Gem::Requirement.new(">= 0"), + source: { + branch: "master", + ref: "master", + type: "git", + url: "http://github.com/gocardless/uk_phone_numbers" + }, + type: :runtime + } + ] + is_expected.to eq(parsed_gemfile) + end + end + end + + describe "#parsed_gemspec" do + let!(:gemspec_fixture) do + fixture("ruby", "gemspecs", "exact") + end + + subject(:parsed_gemspec) do + in_tmp_folder do |tmp_path| + File.write(File.join(tmp_path, "test.gemspec"), gemspec_fixture) + dependency_source.parsed_gemspec(gemspec_name: "test.gemspec") + end + end + + it "parses gemspec" do + parsed_gemspec = [ + { + groups: nil, + name: "business", + requirement: Gem::Requirement.new("= 1.0.0"), + source: nil, + type: :runtime + }, + { + groups: nil, + name: "statesman", + requirement: Gem::Requirement.new("= 1.0.0"), + source: nil, + type: :runtime + } + ] + is_expected.to eq(parsed_gemspec) + end + end +end diff --git a/bundler/helpers/v2/spec/functions_spec.rb b/bundler/helpers/v2/spec/functions_spec.rb index f62d0f9153..8f782d3bdc 100644 --- a/bundler/helpers/v2/spec/functions_spec.rb +++ b/bundler/helpers/v2/spec/functions_spec.rb @@ -5,8 +5,6 @@ RSpec.describe Functions do # Verify v1 method signatures are exist, but raise as NYI { - parsed_gemfile: [ :lockfile_name, :gemfile_name, :dir ], - parsed_gemspec: [ :lockfile_name, :gemspec_name, :dir ], vendor_cache_dir: [ :dir ], update_lockfile: [ :dir, :gemfile_name, :lockfile_name, :using_bundler2, :credentials, :dependencies ], force_update: [ :dir, :dependency_name, :target_version, :gemfile_name, :lockfile_name, :using_bundler2, @@ -19,7 +17,6 @@ :dir, :credentials], jfrog_source: [:dir, :gemfile_name, :credentials, :using_bundler2], git_specs: [:dir, :gemfile_name, :credentials, :using_bundler2], - set_bundler_flags_and_credentials: [:dir, :credentials, :using_bundler2], conflicting_dependencies: [:dir, :dependency_name, :target_version, :lockfile_name, :using_bundler2, :credentials] }.each do |function, kwargs| describe "::#{function}" do diff --git a/bundler/helpers/v2/spec/native_spec_helper.rb b/bundler/helpers/v2/spec/native_spec_helper.rb index 5e97205a85..af5346b916 100644 --- a/bundler/helpers/v2/spec/native_spec_helper.rb +++ b/bundler/helpers/v2/spec/native_spec_helper.rb @@ -5,8 +5,7 @@ require "byebug" $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) -# TODO: Fork `v1/monkey_patches` into `v2/monkey_patches` ? -$LOAD_PATH.unshift(File.expand_path("../../v1/monkey_patches", __dir__)) +$LOAD_PATH.unshift(File.expand_path("../monkey_patches", __dir__)) # Bundler monkey patches require "definition_ruby_version_patch" diff --git a/bundler/helpers/v2/spec/shared_contexts.rb b/bundler/helpers/v2/spec/shared_contexts.rb new file mode 100644 index 0000000000..60dcbd290f --- /dev/null +++ b/bundler/helpers/v2/spec/shared_contexts.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "tmpdir" +require "bundler/compact_index_client" +require "bundler/compact_index_client/updater" + +TMP_DIR_PATH = File.expand_path("../tmp", __dir__) + +RSpec.shared_context "in a temporary bundler directory" do + let(:project_name) { "gemfile" } + + let(:tmp_path) do + Dir.mkdir(TMP_DIR_PATH) unless Dir.exist?(TMP_DIR_PATH) + dir = Dir.mktmpdir("native_helper_spec_", TMP_DIR_PATH) + Pathname.new(dir).expand_path + end + + before do + project_dependency_files(project_name).each do |file| + File.write(File.join(tmp_path, file[:name]), file[:content]) + end + end + + def in_tmp_folder(&block) + Dir.chdir(tmp_path, &block) + end +end + +RSpec.shared_context "without caching rubygems" do + before do + # Stub Bundler to stop it using a cached versions of Rubygems + allow_any_instance_of(Bundler::CompactIndexClient::Updater). + to receive(:etag_for).and_return("") + end +end + +RSpec.shared_context "stub rubygems compact index" do + include_context "without caching rubygems" + + before do + # Stub the Rubygems index + stub_request(:get, "https://index.rubygems.org/versions"). + to_return( + status: 200, + body: fixture("ruby", "rubygems_responses", "index") + ) + + # Stub the Rubygems response for each dependency we have a fixture for + fixtures = + Dir[File.join("../../spec", "fixtures", "ruby", "rubygems_responses", "info-*")] + fixtures.each do |path| + dep_name = path.split("/").last.gsub("info-", "") + stub_request(:get, "https://index.rubygems.org/info/#{dep_name}"). + to_return( + status: 200, + body: fixture("ruby", "rubygems_responses", "info-#{dep_name}") + ) + end + end +end diff --git a/bundler/spec/dependabot/bundler/file_parser_spec.rb b/bundler/spec/dependabot/bundler/file_parser_spec.rb index 8f82e92604..b1c8708a15 100644 --- a/bundler/spec/dependabot/bundler/file_parser_spec.rb +++ b/bundler/spec/dependabot/bundler/file_parser_spec.rb @@ -13,7 +13,8 @@ described_class.new( dependency_files: dependency_files, source: source, - reject_external_code: reject_external_code + reject_external_code: reject_external_code, + options: { bundler_2_available: bundler_2_available? } ) end let(:source) do @@ -209,7 +210,32 @@ end end - describe "a github dependency" do + describe "a github dependency", :bundler_v2_only do + let(:dependency_files) { project_dependency_files("bundler1/github_source") } + + subject { dependencies.find { |d| d.name == "business" } } + let(:expected_requirements) do + [{ + requirement: ">= 0", + file: "Gemfile", + source: { + type: "git", + url: "https://github.com/gocardless/business.git", + branch: "master", + ref: "master" + }, + groups: [:default] + }] + end + + it { is_expected.to be_a(Dependabot::Dependency) } + its(:requirements) { is_expected.to eq(expected_requirements) } + its(:version) do + is_expected.to eq("d31e445215b5af70c1604715d97dd953e868380e") + end + end + + describe "a github dependency", :bundler_v1_only do let(:dependency_files) { project_dependency_files("bundler1/github_source") } subject { dependencies.find { |d| d.name == "business" } } @@ -738,25 +764,4 @@ ) end end - - context "with bundler 2 support enabled" do - let(:parser) do - described_class.new( - dependency_files: dependency_files, - source: source, - reject_external_code: reject_external_code, - options: { - bundler_2_available: true - } - ) - end - - describe "parse" do - it "fails as the native helper is not yet implemented" do - expect { parser.parse }. - to raise_error(Dependabot::NotImplemented, - "Bundler 2 adapter does not yet implement parsed_gemfile") - end - end - end end diff --git a/bundler/spec/spec_helper.rb b/bundler/spec/spec_helper.rb index 6dcb854b37..9972e7065c 100644 --- a/bundler/spec/spec_helper.rb +++ b/bundler/spec/spec_helper.rb @@ -9,3 +9,19 @@ def require_common_spec(path) end require "#{common_dir}/spec/spec_helper.rb" + +def bundler_2_available? + ENV["SUITE_NAME"] == "bundler2" +end + +RSpec.configure do |config| + config.around do |example| + if bundler_2_available? && example.metadata[:bundler_v1_only] + example.skip + elsif !bundler_2_available? && example.metadata[:bundler_v2_only] + example.skip + else + example.run + end + end +end