Skip to content

Commit 90b1758

Browse files
committed
Read npm min-release-age from .npmrc and apply as cooldown floor
- `default_days` now uses `[existing.default_days, npmrc_days].max` so the npmrc value never lowers a higher value set in dependabot.yml - Conflict warning is conditional: only fires when at least one cooldown field is actually below the npmrc threshold; otherwise logs at debug - Removed `default_days` from the per-field override-warning loop; the top-level conflict message covers it; per-field messages are for the three `semver_*_days` fields only - `.npmrc` filename match tightened to `File.basename(f.name) == ".npmrc"` to avoid false matches like `foo.npmrc` - Extracted `log_npmrc_cooldown_conflicts` and `merge_cooldown_with_npmrc_floor` helpers (satisfies Rubocop Metrics cops) - TODO comment added for Yarn 4.10+ `npmMinimalAgeGate` follow-up Spec changes: - "when explicit update_cooldown already exceeds the npmrc floor": asserts default_days stays unchanged and no warning is logged - "when dependabot.yml default_days is below the npmrc floor": expects conflict warning once and per-field warnings for semver fields only - Replace `instance_variable_get(:@update_cooldown)` with `checker.update_cooldown`
1 parent 8c7ebed commit 90b1758

5 files changed

Lines changed: 317 additions & 0 deletions

File tree

npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def initialize( # rubocop:disable Metrics/AbcSize
6565
@package_json = T.let(nil, T.nilable(Dependabot::DependencyFile))
6666
@git_commit_checker = T.let(nil, T.nilable(Dependabot::GitCommitChecker))
6767
super
68+
apply_npmrc_min_release_age
6869
end
6970

7071
sig { returns(T::Boolean) }
@@ -572,6 +573,115 @@ def original_source(updated_dependency)
572573
sources.first
573574
end
574575

576+
# Reads `min-release-age` from .npmrc and applies it as a floor for every
577+
# cooldown field. npm enforces this constraint at install time, so any version
578+
# younger than the threshold will cause `npm install` to fail. When a
579+
# dependabot.yml cooldown is also present, the npmrc value raises any field
580+
# that is below it and leaves higher values unchanged. Include/exclude patterns
581+
# are always dropped because npm applies min-release-age globally with no
582+
# per-package filtering.
583+
sig { void }
584+
def apply_npmrc_min_release_age
585+
npmrc_days = npmrc_min_release_age_days
586+
return unless npmrc_days&.positive?
587+
588+
if @update_cooldown.nil?
589+
@update_cooldown = Dependabot::Package::ReleaseCooldownOptions.new(default_days: npmrc_days)
590+
else
591+
existing = @update_cooldown
592+
log_npmrc_cooldown_conflicts(existing, npmrc_days)
593+
@update_cooldown = merge_cooldown_with_npmrc_floor(existing, npmrc_days)
594+
end
595+
end
596+
597+
sig do
598+
params(
599+
existing: Dependabot::Package::ReleaseCooldownOptions,
600+
npmrc_days: Integer
601+
).void
602+
end
603+
def log_npmrc_cooldown_conflicts(existing, npmrc_days)
604+
if existing.include.any? || existing.exclude.any?
605+
Dependabot.logger.warn(
606+
".npmrc min-release-age does not support include/exclude patterns; " \
607+
"dropping dependabot.yml update_cooldown include/exclude configuration."
608+
)
609+
end
610+
611+
all_days = [existing.default_days, existing.semver_major_days,
612+
existing.semver_minor_days, existing.semver_patch_days]
613+
unless all_days.any? { |days| days < npmrc_days }
614+
Dependabot.logger.debug(
615+
".npmrc min-release-age (#{npmrc_days} days) is already satisfied by all " \
616+
"dependabot.yml update_cooldown values; no adjustment needed."
617+
)
618+
return
619+
end
620+
621+
Dependabot.logger.warn(
622+
".npmrc min-release-age (#{npmrc_days} days) conflicts with dependabot.yml update_cooldown " \
623+
"(default_days: #{existing.default_days}); it acts as a minimum floor for all cooldown values."
624+
)
625+
{ semver_major_days: existing.semver_major_days,
626+
semver_minor_days: existing.semver_minor_days,
627+
semver_patch_days: existing.semver_patch_days }.each do |field, configured_days|
628+
next unless configured_days < npmrc_days
629+
630+
Dependabot.logger.warn(
631+
".npmrc min-release-age (#{npmrc_days} days) overrides dependabot.yml #{field} " \
632+
"(#{configured_days} days) because it would cause npm install to fail."
633+
)
634+
end
635+
end
636+
637+
sig do
638+
params(
639+
existing: Dependabot::Package::ReleaseCooldownOptions,
640+
npmrc_days: Integer
641+
).returns(Dependabot::Package::ReleaseCooldownOptions)
642+
end
643+
def merge_cooldown_with_npmrc_floor(existing, npmrc_days)
644+
Dependabot::Package::ReleaseCooldownOptions.new(
645+
default_days: [existing.default_days, npmrc_days].max,
646+
semver_major_days: [existing.semver_major_days, npmrc_days].max,
647+
semver_minor_days: [existing.semver_minor_days, npmrc_days].max,
648+
semver_patch_days: [existing.semver_patch_days, npmrc_days].max,
649+
include: [],
650+
exclude: []
651+
)
652+
end
653+
654+
sig { returns(T.nilable(Integer)) }
655+
def npmrc_min_release_age_days
656+
npmrc_file = dependency_files.find { |f| File.basename(f.name) == ".npmrc" }
657+
unless npmrc_file&.content
658+
Dependabot.logger.debug("No .npmrc file found; skipping min-release-age check.")
659+
return nil
660+
end
661+
662+
T.must(npmrc_file.content).split("\n").each do |line|
663+
days = parse_min_release_age_line(line, npmrc_file.name)
664+
return days if days
665+
end
666+
Dependabot.logger.debug("No min-release-age key found in #{npmrc_file.name}.")
667+
nil
668+
end
669+
670+
sig { params(line: String, filename: String).returns(T.nilable(Integer)) }
671+
def parse_min_release_age_line(line, filename)
672+
key, value = line.strip.split("=", 2)
673+
return nil unless key&.strip == "min-release-age" && value
674+
675+
parsed = T.let(Integer(value.strip, 10, exception: false), T.nilable(Integer))
676+
if parsed&.positive?
677+
Dependabot.logger.debug("Found min-release-age=#{parsed} days in #{filename}.")
678+
parsed
679+
else
680+
Dependabot.logger.debug("Ignoring invalid min-release-age value '#{value.strip}' in #{filename}.")
681+
nil
682+
end
683+
end
684+
575685
sig { returns(T.nilable(Dependabot::DependencyFile)) }
576686
def package_json
577687
@package_json ||=

npm_and_yarn/spec/dependabot/npm_and_yarn/update_checker_spec.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2395,4 +2395,110 @@ def eq_including_metadata(expected_array)
23952395
expect(updated_deps[0].name).to eq("is-stream")
23962396
end
23972397
end
2398+
2399+
describe "npmrc min-release-age cooldown" do
2400+
let(:dependency_files) { project_dependency_files("npm6/npmrc_min_release_age") }
2401+
2402+
it "creates a cooldown from the npmrc min-release-age value" do
2403+
expect(checker.update_cooldown).to be_a(Dependabot::Package::ReleaseCooldownOptions)
2404+
expect(checker.update_cooldown.default_days).to eq(3)
2405+
end
2406+
2407+
context "when an explicit update_cooldown already exceeds the npmrc floor" do
2408+
let(:checker) do
2409+
described_class.new(
2410+
dependency: dependency,
2411+
dependency_files: dependency_files,
2412+
credentials: credentials,
2413+
update_cooldown: Dependabot::Package::ReleaseCooldownOptions.new(default_days: 10),
2414+
options: options
2415+
)
2416+
end
2417+
2418+
it "keeps default_days unchanged and logs no warning" do
2419+
expect(Dependabot.logger).not_to receive(:warn)
2420+
expect(checker.update_cooldown.default_days).to eq(10)
2421+
end
2422+
end
2423+
2424+
context "when dependabot.yml default_days is below the npmrc floor" do
2425+
let(:checker) do
2426+
described_class.new(
2427+
dependency: dependency,
2428+
dependency_files: dependency_files,
2429+
credentials: credentials,
2430+
update_cooldown: Dependabot::Package::ReleaseCooldownOptions.new(default_days: 1),
2431+
options: options
2432+
)
2433+
end
2434+
2435+
it "raises default_days to the npmrc floor and logs semver-field warnings only" do
2436+
expect(Dependabot.logger).to receive(:warn).with(
2437+
".npmrc min-release-age (3 days) conflicts with dependabot.yml update_cooldown " \
2438+
"(default_days: 1); it acts as a minimum floor for all cooldown values."
2439+
).once
2440+
# ReleaseCooldownOptions derives semver fields from default_days when not set
2441+
# explicitly, so all three are 1 and each gets an override warning.
2442+
%w(semver_major_days semver_minor_days semver_patch_days).each do |field|
2443+
expect(Dependabot.logger).to receive(:warn).with(
2444+
".npmrc min-release-age (3 days) overrides dependabot.yml #{field} " \
2445+
"(1 days) because it would cause npm install to fail."
2446+
)
2447+
end
2448+
expect(checker.update_cooldown.default_days).to eq(3)
2449+
end
2450+
end
2451+
2452+
context "when a semver-specific day is below the npmrc floor" do
2453+
let(:checker) do
2454+
described_class.new(
2455+
dependency: dependency,
2456+
dependency_files: dependency_files,
2457+
credentials: credentials,
2458+
update_cooldown: Dependabot::Package::ReleaseCooldownOptions.new(
2459+
default_days: 10,
2460+
semver_patch_days: 1
2461+
),
2462+
options: options
2463+
)
2464+
end
2465+
2466+
it "raises semver_patch_days to the npmrc floor and logs an override warning" do
2467+
expect(Dependabot.logger).to receive(:warn).with(
2468+
".npmrc min-release-age (3 days) conflicts with dependabot.yml update_cooldown " \
2469+
"(default_days: 10); it acts as a minimum floor for all cooldown values."
2470+
)
2471+
expect(Dependabot.logger).to receive(:warn).with(
2472+
".npmrc min-release-age (3 days) overrides dependabot.yml semver_patch_days " \
2473+
"(1 days) because it would cause npm install to fail."
2474+
)
2475+
expect(checker.update_cooldown.semver_patch_days).to eq(3)
2476+
end
2477+
end
2478+
2479+
context "when include/exclude patterns are configured alongside npmrc" do
2480+
let(:checker) do
2481+
described_class.new(
2482+
dependency: dependency,
2483+
dependency_files: dependency_files,
2484+
credentials: credentials,
2485+
update_cooldown: Dependabot::Package::ReleaseCooldownOptions.new(
2486+
default_days: 5,
2487+
include: %w(lodash react)
2488+
),
2489+
options: options
2490+
)
2491+
end
2492+
2493+
it "drops include/exclude and logs a warning" do
2494+
expect(Dependabot.logger).to receive(:warn).with(
2495+
".npmrc min-release-age does not support include/exclude patterns; " \
2496+
"dropping dependabot.yml update_cooldown include/exclude configuration."
2497+
)
2498+
expect(checker.update_cooldown.include).to be_empty
2499+
expect(checker.update_cooldown.exclude).to be_empty
2500+
expect(checker.update_cooldown.default_days).to eq(5)
2501+
end
2502+
end
2503+
end
23982504
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
min-release-age=3

npm_and_yarn/spec/fixtures/projects/npm6/npmrc_min_release_age/package-lock.json

Lines changed: 75 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "{{ name }}",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no\\ test\\ specified\" && exit 1",
8+
"prettify": "prettier --write \"{{packages/*/src,examples,cypress,scripts}/**/,}*.{js,jsx,ts,tsx,css,md}\""
9+
},
10+
"repository": {
11+
"type": "git",
12+
"url": "git+https://github.com/waltfy/PROTO_TEST.git"
13+
},
14+
"author": "",
15+
"license": "ISC",
16+
"bugs": {
17+
"url": "https://github.com/waltfy/PROTO_TEST/issues"
18+
},
19+
"homepage": "https://github.com/waltfy/PROTO_TEST#readme",
20+
"dependencies": {
21+
"fetch-factory": "^0.0.1"
22+
},
23+
"devDependencies": {
24+
"etag" : "^1.0.0"
25+
}}

0 commit comments

Comments
 (0)