Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def initialize( # rubocop:disable Metrics/AbcSize
@package_json = T.let(nil, T.nilable(Dependabot::DependencyFile))
@git_commit_checker = T.let(nil, T.nilable(Dependabot::GitCommitChecker))
super
apply_npmrc_min_release_age
end

sig { returns(T::Boolean) }
Expand Down Expand Up @@ -572,6 +573,115 @@ def original_source(updated_dependency)
sources.first
end

# Reads `min-release-age` from .npmrc and applies it as a floor for every
# cooldown field. npm enforces this constraint at install time, so any version
# younger than the threshold will cause `npm install` to fail. When a
# dependabot.yml cooldown is also present, the npmrc value raises any field
# that is below it and leaves higher values unchanged. Include/exclude patterns
# are always dropped because npm applies min-release-age globally with no
# per-package filtering.
sig { void }
def apply_npmrc_min_release_age
npmrc_days = npmrc_min_release_age_days
return unless npmrc_days&.positive?

if @update_cooldown.nil?
@update_cooldown = Dependabot::Package::ReleaseCooldownOptions.new(default_days: npmrc_days)
else
existing = @update_cooldown
log_npmrc_cooldown_conflicts(existing, npmrc_days)
@update_cooldown = merge_cooldown_with_npmrc_floor(existing, npmrc_days)
end
end

sig do
params(
existing: Dependabot::Package::ReleaseCooldownOptions,
npmrc_days: Integer
).void
end
def log_npmrc_cooldown_conflicts(existing, npmrc_days)
if existing.include.any? || existing.exclude.any?
Dependabot.logger.warn(
".npmrc min-release-age does not support include/exclude patterns; " \
"dropping dependabot.yml update_cooldown include/exclude configuration."
)
end

all_days = [existing.default_days, existing.semver_major_days,
existing.semver_minor_days, existing.semver_patch_days]
unless all_days.any? { |days| days < npmrc_days }
Dependabot.logger.debug(
".npmrc min-release-age (#{npmrc_days} days) is already satisfied by all " \
"dependabot.yml update_cooldown values; no adjustment needed."
)
return
end

Dependabot.logger.warn(
".npmrc min-release-age (#{npmrc_days} days) conflicts with dependabot.yml update_cooldown " \
"(default_days: #{existing.default_days}); it acts as a minimum floor for all cooldown values."
)
{ semver_major_days: existing.semver_major_days,
semver_minor_days: existing.semver_minor_days,
semver_patch_days: existing.semver_patch_days }.each do |field, configured_days|
next unless configured_days < npmrc_days

Dependabot.logger.warn(
".npmrc min-release-age (#{npmrc_days} days) overrides dependabot.yml #{field} " \
"(#{configured_days} days) because it would cause npm install to fail."
)
end
end

sig do
params(
existing: Dependabot::Package::ReleaseCooldownOptions,
npmrc_days: Integer
).returns(Dependabot::Package::ReleaseCooldownOptions)
end
def merge_cooldown_with_npmrc_floor(existing, npmrc_days)
Dependabot::Package::ReleaseCooldownOptions.new(
default_days: [existing.default_days, npmrc_days].max,
semver_major_days: [existing.semver_major_days, npmrc_days].max,
semver_minor_days: [existing.semver_minor_days, npmrc_days].max,
semver_patch_days: [existing.semver_patch_days, npmrc_days].max,
include: [],
exclude: []
)
end

sig { returns(T.nilable(Integer)) }
def npmrc_min_release_age_days
npmrc_file = dependency_files.find { |f| File.basename(f.name) == ".npmrc" }
unless npmrc_file&.content
Dependabot.logger.debug("No .npmrc file found; skipping min-release-age check.")
return nil
end

T.must(npmrc_file.content).split("\n").each do |line|
days = parse_min_release_age_line(line, npmrc_file.name)
return days if days
end
Dependabot.logger.debug("No min-release-age key found in #{npmrc_file.name}.")
nil
end

sig { params(line: String, filename: String).returns(T.nilable(Integer)) }
def parse_min_release_age_line(line, filename)
key, value = line.strip.split("=", 2)
return nil unless key&.strip == "min-release-age" && value

parsed = T.let(Integer(value.strip, 10, exception: false), T.nilable(Integer))
if parsed&.positive?
Dependabot.logger.debug("Found min-release-age=#{parsed} days in #{filename}.")
parsed
else
Dependabot.logger.debug("Ignoring invalid min-release-age value '#{value.strip}' in #{filename}.")
nil
end
end

sig { returns(T.nilable(Dependabot::DependencyFile)) }
def package_json
@package_json ||=
Expand Down
106 changes: 106 additions & 0 deletions npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2395,4 +2395,110 @@ def eq_including_metadata(expected_array)
expect(updated_deps[0].name).to eq("is-stream")
end
end

describe "npmrc min-release-age cooldown" do
let(:dependency_files) { project_dependency_files("npm6/npmrc_min_release_age") }

it "creates a cooldown from the npmrc min-release-age value" do
expect(checker.update_cooldown).to be_a(Dependabot::Package::ReleaseCooldownOptions)
expect(checker.update_cooldown.default_days).to eq(3)
end

context "when an explicit update_cooldown already exceeds the npmrc floor" do
let(:checker) do
described_class.new(
dependency: dependency,
dependency_files: dependency_files,
credentials: credentials,
update_cooldown: Dependabot::Package::ReleaseCooldownOptions.new(default_days: 10),
options: options
)
end

it "keeps default_days unchanged and logs no warning" do
expect(Dependabot.logger).not_to receive(:warn)
expect(checker.update_cooldown.default_days).to eq(10)
end
end

context "when dependabot.yml default_days is below the npmrc floor" do
let(:checker) do
described_class.new(
dependency: dependency,
dependency_files: dependency_files,
credentials: credentials,
update_cooldown: Dependabot::Package::ReleaseCooldownOptions.new(default_days: 1),
options: options
)
end

it "raises default_days to the npmrc floor and logs semver-field warnings only" do
expect(Dependabot.logger).to receive(:warn).with(
".npmrc min-release-age (3 days) conflicts with dependabot.yml update_cooldown " \
"(default_days: 1); it acts as a minimum floor for all cooldown values."
).once
# ReleaseCooldownOptions derives semver fields from default_days when not set
# explicitly, so all three are 1 and each gets an override warning.
%w(semver_major_days semver_minor_days semver_patch_days).each do |field|
expect(Dependabot.logger).to receive(:warn).with(
".npmrc min-release-age (3 days) overrides dependabot.yml #{field} " \
"(1 days) because it would cause npm install to fail."
)
end
expect(checker.update_cooldown.default_days).to eq(3)
end
end

context "when a semver-specific day is below the npmrc floor" do
let(:checker) do
described_class.new(
dependency: dependency,
dependency_files: dependency_files,
credentials: credentials,
update_cooldown: Dependabot::Package::ReleaseCooldownOptions.new(
default_days: 10,
semver_patch_days: 1
),
options: options
)
end

it "raises semver_patch_days to the npmrc floor and logs an override warning" do
expect(Dependabot.logger).to receive(:warn).with(
".npmrc min-release-age (3 days) conflicts with dependabot.yml update_cooldown " \
"(default_days: 10); it acts as a minimum floor for all cooldown values."
)
expect(Dependabot.logger).to receive(:warn).with(
".npmrc min-release-age (3 days) overrides dependabot.yml semver_patch_days " \
"(1 days) because it would cause npm install to fail."
)
expect(checker.update_cooldown.semver_patch_days).to eq(3)
end
end

context "when include/exclude patterns are configured alongside npmrc" do
let(:checker) do
described_class.new(
dependency: dependency,
dependency_files: dependency_files,
credentials: credentials,
update_cooldown: Dependabot::Package::ReleaseCooldownOptions.new(
default_days: 5,
include: %w(lodash react)
),
options: options
)
end

it "drops include/exclude and logs a warning" do
expect(Dependabot.logger).to receive(:warn).with(
".npmrc min-release-age does not support include/exclude patterns; " \
"dropping dependabot.yml update_cooldown include/exclude configuration."
)
expect(checker.update_cooldown.include).to be_empty
expect(checker.update_cooldown.exclude).to be_empty
expect(checker.update_cooldown.default_days).to eq(5)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
min-release-age=3

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "{{ name }}",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no\\ test\\ specified\" && exit 1",
"prettify": "prettier --write \"{{packages/*/src,examples,cypress,scripts}/**/,}*.{js,jsx,ts,tsx,css,md}\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/waltfy/PROTO_TEST.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/waltfy/PROTO_TEST/issues"
},
"homepage": "https://github.com/waltfy/PROTO_TEST#readme",
"dependencies": {
"fetch-factory": "^0.0.1"
},
"devDependencies": {
"etag" : "^1.0.0"
}}
Loading