The first post in this series covers why I wait two weeks before installing any new release. npm, pnpm, and yarn have a "minimum release age" setting for that. Bundler has no built-in equivalent, so in a Ruby project you have to enforce it yourself.
Pin, update, unpin
Bundler resolves to the newest version that satisfies the Gemfile, so to get older versions, temporarily cap the gems that are too new:
gem 'json', '< 3.1'
gem 'prism', '< 2.0'
gem 'sass-embedded', '<= 1.105.1'Then:
bundle update --allOnce the lockfile looks right, delete the pins from the Gemfile and run bundle lock with no --update. Loosening a constraint doesn't force Bundler to re-resolve, so it keeps the versions already in Gemfile.lock. The Gemfile stays clean, and the lockfile holds the cooldown-compliant versions.
Verify every locked gem against its publish date
Pinning only covers the gems in your Gemfile. The gems those gems depend on can still update to something too new, and you won't notice unless you check. The only reliable check is the publish date of the exact version in the lockfile.
Native gems like nokogiri, sass-embedded, google-protobuf, and ffi add a wrinkle. They ship per-platform builds (arm64-darwin, x86_64-linux-gnu), each build has its own created_at, and builds of the same version can be published on different days. So the check has to match on both version and platform.
This script reads Gemfile.lock with Bundler's own parser, looks up each gem in the RubyGems API, and reports anything newer than the cutoff or missing entirely. It includes the Bundler version from BUNDLED WITH, and skips gems from git or path sources:
# Usage: ruby check_cooldown.rb [Gemfile.lock] [days]
require "bundler"
require "date"
require "json"
require "net/http"
lockfile = ARGV[0] || "Gemfile.lock"
days = Integer(ARGV[1] || 14)
cutoff = Date.today - days
parser = Bundler::LockfileParser.new(File.read(lockfile))
specs = parser.specs
.select { |s| s.source.is_a?(Bundler::Source::Rubygems) }
.map { |s| [s.name, s.version.to_s, s.platform.to_s] }
specs << ["bundler", parser.bundler_version.to_s, "ruby"] if parser.bundler_version
too_new = specs.uniq.filter_map do |name, version, platform|
url = URI("https://rubygems.org/api/v1/versions/#{name}.json")
versions = JSON.parse(Net::HTTP.get(url)) rescue [] # private or unknown gem
release = versions.find do |v|
v["number"] == version && v["platform"] == platform
end
published = release && Date.parse(release["created_at"])
"#{name} #{version} (#{platform}): #{published || 'not found'}" if published.nil? || published > cutoff
end
if too_new.empty?
puts "All #{specs.uniq.size} locked gems are at least #{days} days old."
else
puts too_new
exit 1
endRun it with bundle exec ruby check_cooldown.rb. If it lists anything, add a pin for each gem, run bundle update --all again, and re-check. It exits non-zero when something is too new, so you can also run it in CI and keep the cooldown on every dependency change, not just the ones where you remembered.
Don't forget JavaScript dependencies
If the project also has a JS lockfile, install from it without running install scripts, since that's how Shai-Hulud spread. The npm post covers the rest.
yarn install --frozen-lockfile --ignore-scripts