diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater.rb index 43a47ebdff..5ccd91ef23 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater.rb @@ -469,7 +469,8 @@ def npm_lockfile_updater_for(file) lockfile: file, dependencies: dependencies, dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + security_updates_only: options.fetch(:security_updates_only, false) ) end diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb index 3f181e4fbd..dcf6db2451 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb @@ -29,15 +29,17 @@ class NpmLockfileUpdater lockfile: Dependabot::DependencyFile, dependencies: T::Array[Dependabot::Dependency], dependency_files: T::Array[Dependabot::DependencyFile], - credentials: T::Array[Credential] + credentials: T::Array[Credential], + security_updates_only: T::Boolean ) .void end - def initialize(lockfile:, dependencies:, dependency_files:, credentials:) + def initialize(lockfile:, dependencies:, dependency_files:, credentials:, security_updates_only: false) @lockfile = lockfile @dependencies = dependencies @dependency_files = dependency_files @credentials = credentials + @security_updates_only = T.let(security_updates_only, T::Boolean) end sig { returns(Dependabot::DependencyFile) } @@ -72,6 +74,11 @@ def updated_lockfile_reponse(response) sig { returns(T::Array[Credential]) } attr_reader :credentials + sig { returns(T::Boolean) } + def security_updates_only? + @security_updates_only + end + UNREACHABLE_GIT = /fatal: repository '(?.*)' not found/ FORBIDDEN_GIT = /fatal: Authentication failed for '(?.*)'/ FORBIDDEN_PACKAGE = %r{(?[^/]+) - (Forbidden|Unauthorized)} @@ -394,6 +401,9 @@ def run_npm_install_lockfile_only(install_args = [], has_optional_dependencies: ] command_args << "--save-optional" if has_optional_dependencies + # Override any min-release-age set in .npmrc: security fixes must not be + # blocked by a release-age gate the user configured for regular updates. + command_args << "--min-release-age=0" if security_updates_only? command = command_args.join(" ") @@ -406,6 +416,7 @@ def run_npm_install_lockfile_only(install_args = [], has_optional_dependencies: ] fingerprint_args << "--save-optional" if has_optional_dependencies + fingerprint_args << "--min-release-age=0" if security_updates_only? fingerprint = fingerprint_args.join(" ") diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater_spec.rb index 74b06e7e7e..b122beb30a 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater_spec.rb @@ -1360,6 +1360,46 @@ end end + describe "#run_npm_install_lockfile_only" do + let(:files) { project_dependency_files("npm8/simple") } + let(:install_args) { ["lodash@4.18.1"] } + + context "when security_updates_only is true" do + let(:updater) do + described_class.new( + lockfile: package_lock, + dependency_files: files, + dependencies: dependencies, + credentials: credentials, + security_updates_only: true + ) + end + + it "passes --min-release-age=0 to override the .npmrc setting" do + expect(Dependabot::NpmAndYarn::Helpers).to receive(:run_npm_command) do |command, _options| + expect(command).to include("--min-release-age=0") + expect(command).to include("--package-lock-only") + expect(command).to include("--force") + "" + end + + updater.send(:run_npm_install_lockfile_only, install_args) + end + end + + context "when security_updates_only is false (default)" do + it "does not pass --min-release-age=0" do + expect(Dependabot::NpmAndYarn::Helpers).to receive(:run_npm_command) do |command, _options| + expect(command).not_to include("--min-release-age=0") + expect(command).to include("--package-lock-only") + "" + end + + updater.send(:run_npm_install_lockfile_only, install_args) + end + end + end + describe "#optional_dependency?" do it "correctly identifies optional dependencies" do optional_dep = Dependabot::Dependency.new( diff --git a/updater/lib/dependabot/dependency_change_builder.rb b/updater/lib/dependabot/dependency_change_builder.rb index 59398760f1..9771e24e11 100644 --- a/updater/lib/dependabot/dependency_change_builder.rb +++ b/updater/lib/dependabot/dependency_change_builder.rb @@ -189,7 +189,7 @@ def file_updater_for(dependencies) dependency_files: dependency_files, repo_contents_path: job.repo_contents_path, credentials: job.credentials, - options: job.experiments + options: job.experiments.merge(security_updates_only: job.security_updates_only?) ) end end diff --git a/updater/spec/dependabot/dependency_change_builder_spec.rb b/updater/spec/dependabot/dependency_change_builder_spec.rb index 52a7160f32..2eda65932a 100644 --- a/updater/spec/dependabot/dependency_change_builder_spec.rb +++ b/updater/spec/dependabot/dependency_change_builder_spec.rb @@ -23,6 +23,7 @@ } ], experiments: {}, + security_updates_only?: false, source: source ) end @@ -125,6 +126,40 @@ def dependency_group_source Dependabot::DependencyGroup.new(name: "dummy-pkg-*", rules: { patterns: ["dummy-pkg-*"] }) end + context "when the job is a security update" do + let(:change_source) { lead_dependency_change_source } + + before do + allow(job).to receive(:security_updates_only?).and_return(true) + stub_file_updater(updated_dependency_files: dependency_files.reject(&:support_file?)) + end + + it "passes security_updates_only: true in options to the file updater" do + create_change + + expect(file_updater_class).to have_received(:new).with( + hash_including(options: hash_including(security_updates_only: true)) + ) + end + end + + context "when the job is not a security update" do + let(:change_source) { lead_dependency_change_source } + + before do + allow(job).to receive(:security_updates_only?).and_return(false) + stub_file_updater(updated_dependency_files: dependency_files.reject(&:support_file?)) + end + + it "passes security_updates_only: false in options to the file updater" do + create_change + + expect(file_updater_class).to have_received(:new).with( + hash_including(options: hash_including(security_updates_only: false)) + ) + end + end + context "when the source is a lead dependency" do let(:change_source) { lead_dependency_change_source } diff --git a/updater/spec/dependabot/updater_spec.rb b/updater/spec/dependabot/updater_spec.rb index a38318644d..ba7b1d8114 100644 --- a/updater/spec/dependabot/updater_spec.rb +++ b/updater/spec/dependabot/updater_spec.rb @@ -660,7 +660,7 @@ def expect_update_checker_with_ignored_versions(versions, dependency_matcher: an dependency_files: default_dependency_files, repo_contents_path: nil, credentials: anything, - options: { cloning: true } + options: hash_including(cloning: true) ).and_call_original expect(service).to receive(:create_pull_request).once @@ -2165,7 +2165,7 @@ def expect_update_checker_with_ignored_versions(versions, dependency_matcher: an ], repo_contents_path: nil, credentials: anything, - options: { large_hadron_collider: true } + options: hash_including(large_hadron_collider: true) ).and_call_original updater.run