Skip to content
Merged
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
16 changes: 16 additions & 0 deletions bundler/lib/bundler/resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -456,11 +456,27 @@ def cooldown_hint(specs)
def cooldown_excluded?(spec)
return false unless spec.respond_to?(:created_at) && spec.created_at
return false unless spec.respond_to?(:remote) && spec.remote
return false if pinned_by_lockfile_floor?(spec)
days = spec.remote.effective_cooldown
return false if days.nil? || days <= 0
(cooldown_now - spec.created_at) < (days * 86_400)
end

# A spec sitting exactly at a `>= locked_version` prevent-downgrade floor is
# the version the lockfile currently pins. `bundle update` and `bundle
# outdated` install that floor so resolution never moves a gem backwards.
# Filtering it out for cooldown would then make resolution impossible
# whenever the locked version is itself inside the cooldown window, which is
# exactly what happens to a lockfile written before cooldown was enabled.
# Keep it eligible; gems being explicitly updated carry an exact `=`
# requirement instead and stay subject to the cooldown filter.
def pinned_by_lockfile_floor?(spec)
return false unless defined?(@base) && @base
requirement = base_requirements[spec.name]
return false unless requirement && !requirement.exact?
requirement.requirements.any? {|op, version| op == ">=" && version == spec.version }
end

def cooldown_now
@cooldown_now ||= Time.now
end
Expand Down
161 changes: 161 additions & 0 deletions spec/install/cooldown_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@
build_gem "ripe_gem", "2.0.0" do |s|
s.date = now - (1 * 86_400)
end

# parent only resolves with the in-cooldown child 2.0.0
build_gem "child", "1.0.0" do |s|
s.date = now - (30 * 86_400)
end
build_gem "child", "2.0.0" do |s|
s.date = now - (1 * 86_400)
end
build_gem "parent", "1.0.0" do |s|
s.add_dependency "child", ">= 2.0.0"
s.date = now - (30 * 86_400)
end

# a cooldown-eligible version exists above the in-cooldown locked one
build_gem "upgradable", "2.0.0" do |s|
s.date = now - (1 * 86_400)
end
build_gem "upgradable", "3.0.0" do |s|
s.date = now - (30 * 86_400)
end
end
end

Expand Down Expand Up @@ -268,5 +288,146 @@
expect(err).to match(/excluded by the cooldown setting/)
expect(err).to match(/--cooldown 0/)
end

it "keeps an in-cooldown locked version on bundle update --all instead of failing" do
# Lockfile written before cooldown was enabled pins the now-in-cooldown
# latest version. A full update must not downgrade below it, and cooldown
# must not filter it out, otherwise resolution becomes impossible (#9598).
gemfile <<-G
source "https://gem.repo3"
gem "ripe_gem"
G

lockfile <<-L
GEM
remote: https://gem.repo3/
specs:
ripe_gem (2.0.0)

PLATFORMS
#{lockfile_platforms}

DEPENDENCIES
ripe_gem

BUNDLED WITH
#{Bundler::VERSION}
L

bundle "update --all --cooldown 7", artifice: "compact_index_cooldown"

expect(the_bundle).to include_gems("ripe_gem 2.0.0")
end

it "does not fail bundle outdated when the locked version is in cooldown" do
gemfile <<-G
source "https://gem.repo3"
gem "ripe_gem"
G

lockfile <<-L
GEM
remote: https://gem.repo3/
specs:
ripe_gem (2.0.0)

PLATFORMS
#{lockfile_platforms}

DEPENDENCIES
ripe_gem

BUNDLED WITH
#{Bundler::VERSION}
L

bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false

# exit 0 means no outdated gems and, crucially, no resolution failure (exit 7)
expect(exitstatus).to eq(0)
end

it "still applies cooldown and downgrades a gem that is updated explicitly" do
gemfile <<-G
source "https://gem.repo3"
gem "ripe_gem"
G

lockfile <<-L
GEM
remote: https://gem.repo3/
specs:
ripe_gem (2.0.0)

PLATFORMS
#{lockfile_platforms}

DEPENDENCIES
ripe_gem

BUNDLED WITH
#{Bundler::VERSION}
L

bundle "update ripe_gem --cooldown 7", artifice: "compact_index_cooldown"

expect(the_bundle).to include_gems("ripe_gem 1.0.0")
end

it "keeps an in-cooldown transitive dependency on bundle update --all" do
gemfile <<-G
source "https://gem.repo3"
gem "parent"
G

lockfile <<-L
GEM
remote: https://gem.repo3/
specs:
child (2.0.0)
parent (1.0.0)
child (>= 2.0.0)

PLATFORMS
#{lockfile_platforms}

DEPENDENCIES
parent

BUNDLED WITH
#{Bundler::VERSION}
L

bundle "update --all --cooldown 7", artifice: "compact_index_cooldown"

expect(the_bundle).to include_gems("parent 1.0.0", "child 2.0.0")
end

it "still upgrades to a cooldown-eligible version above the locked one" do
gemfile <<-G
source "https://gem.repo3"
gem "upgradable"
G

lockfile <<-L
GEM
remote: https://gem.repo3/
specs:
upgradable (2.0.0)

PLATFORMS
#{lockfile_platforms}

DEPENDENCIES
upgradable

BUNDLED WITH
#{Bundler::VERSION}
L

bundle "update --all --cooldown 7", artifice: "compact_index_cooldown"

expect(the_bundle).to include_gems("upgradable 3.0.0")
end
end
end
Loading