Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
db2eeca
Merge pull request #9337 from ruby/show-date-outdated
hsbt Mar 3, 2026
2fb61bd
Fallback to copy symlinks on Windows (#9296)
larskanis Mar 25, 2026
52f1275
Merge pull request #9540 from Shopify/rwstauner/stdin-close
hsbt May 14, 2026
a68940d
Merge pull request #9493 from thesmartshadow/fix/cwe59-symlink-extract
hsbt May 26, 2026
2a6f0fa
Merge pull request #9310 from ngan/fix-stale-cache-after-process-lock
hsbt May 26, 2026
62919fe
Merge pull request #9557 from maxfelsher-cgi/fix-install-permissions
hsbt May 26, 2026
ccb18f1
Merge pull request #9574 from ruby/vendor-compact-index
hsbt May 29, 2026
e505c0b
Merge pull request #9578 from ruby/install-compact-index
hsbt Jun 1, 2026
18fbb14
Merge pull request #9579 from ruby/compact-index-install-lock
hsbt Jun 1, 2026
bd0f525
Merge pull request #9581 from ruby/atomic-write-vendored-compact-index
hsbt Jun 1, 2026
92617c5
Merge pull request #9580 from ruby/scrub-invalid-utf8-command-output
hsbt Jun 1, 2026
291756a
Merge pull request #9576 from ruby/cooldown-feature
hsbt Jun 2, 2026
f09e449
Merge pull request #9583 from ruby/endpoint-spec-time-new
hsbt Jun 2, 2026
e31cf91
Merge pull request #9582 from ruby/cooldown-local-stub-bypass
hsbt Jun 2, 2026
299d5da
Merge pull request #9585 from ruby/split-compact-index-entry-on-first…
hsbt Jun 2, 2026
49aebc8
Changelog for Bundler version 4.0.13
hsbt Jun 3, 2026
15c58aa
Bump Bundler version to 4.0.13
hsbt Jun 3, 2026
2da6b87
Changelog for Rubygems version 4.0.13
hsbt Jun 3, 2026
003f20f
Bump Rubygems version to 4.0.13
hsbt Jun 3, 2026
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
14 changes: 13 additions & 1 deletion .github/workflows/rubygems.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ jobs:
- ruby: { name: truffleruby, value: truffleruby-24.2.1 }
os: { name: Ubuntu, value: ubuntu-24.04 }

- ruby: { name: no symlinks, value: 4.0.0 }
os: { name: Windows, value: windows-2025 }
symlink: off

steps:
- name: disable development mode on Windows
run: powershell -c "Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock -Name AllowDevelopmentWithoutDevLicense -Value 0"
if: matrix.symlink == 'off'
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
Expand All @@ -52,7 +59,7 @@ jobs:
run: bin/rake setup
- name: Run Test
run: bin/rake test
if: matrix.ruby.name != 'truffleruby' && matrix.ruby.name != 'jruby'
if: matrix.ruby.name != 'truffleruby' && matrix.ruby.name != 'jruby' && matrix.symlink != 'off'
- name: Run Test isolatedly
run: bin/rake test:isolated
if: matrix.ruby.name == '3.4' && matrix.os.name != 'Windows'
Expand All @@ -62,6 +69,11 @@ jobs:
- name: Run Test (Truffleruby)
run: TRUFFLERUBYOPT="--experimental-options --testing-rubygems" bin/rake test
if: matrix.ruby.name == 'truffleruby'
- name: Run Test with non-Admin user
run: |
gem inst win32-process --no-doc --conservative
ruby bin/windows_run_as_user ruby -S rake test
if: matrix.symlink == 'off'

timeout-minutes: 60

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 4.0.13 / 2026-06-03

### Enhancements:

* Prevent extraction from escaping destination_dir via pre-existing symlinks. Pull request [#9493](https://github.com/ruby/rubygems/pull/9493) by thesmartshadow
* Close stdin immediately when using popen2e. Pull request [#9540](https://github.com/ruby/rubygems/pull/9540) by rwstauner
* Fallback to copy symlinks on Windows. Pull request [#9296](https://github.com/ruby/rubygems/pull/9296) by larskanis
* Installs bundler 4.0.13 as a default gem.

## 4.0.12 / 2026-05-20

### Enhancements:
Expand Down
36 changes: 36 additions & 0 deletions bin/windows_run_as_user
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require "win32/process"
require "rbconfig"

testuser = "testuser"
testpassword = "Password123+"

# Remove a previous test user if present
# See https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/net-user
system("net user #{testuser} /del 2>NUL")
# Create a new non-admin user
system("net user #{testuser} \"#{testpassword}\" /add")

pinfo = nil
IO.pipe do |stdout_read, stdout_write|
cmd = ARGV.join(" ")
env = {
"TMP" => "#{Dir.pwd}/tmp",
"TEMP" => "#{Dir.pwd}/tmp"
}
pinfo = Process.create command_line: cmd,
with_logon: testuser,
password: testpassword,
cwd: Dir.pwd,
environment: ENV.to_h.merge(env).map{|k,v| "#{k}=#{v}" },
startup_info: { stdout: stdout_write, stderr: stdout_write }

stdout_write.close
stdout_read.each_line do |line|
puts(line)
end
end

# Wait for process to terminate
sleep 1 while !(ecode=Process.get_exitcode(pinfo.process_id))

exit ecode
16 changes: 16 additions & 0 deletions bundler/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 4.0.13 / 2026-06-03

### Enhancements:

* Do not hard-code permissions for new gem directories during bundle install. Pull request [#9557](https://github.com/ruby/rubygems/pull/9557) by maxfelsher-cgi
* Clear gem specification cache after acquiring process lock. Pull request [#9310](https://github.com/ruby/rubygems/pull/9310) by ngan
* Show release date with bundle outdated. Pull request [#9337](https://github.com/ruby/rubygems/pull/9337) by hsbt

### Bug fixes:

* Apply cooldown to locally installed gem versions. Pull request [#9582](https://github.com/ruby/rubygems/pull/9582) by hsbt

### Security:

* Add `cooldown` to delay newly published gem. Pull request [#9576](https://github.com/ruby/rubygems/pull/9576) by hsbt

## 4.0.12 / 2026-05-20

### Enhancements:
Expand Down
4 changes: 4 additions & 0 deletions bundler/lib/bundler/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ def remove(*gems)
method_option "target-rbconfig", type: :string, banner: "Path to rbconfig.rb for the deployment target platform"
method_option "without", type: :array, banner: "Exclude gems that are part of the specified named group (removed)."
method_option "with", type: :array, banner: "Include gems that are part of the specified named group (removed)."
method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
def install
%w[clean deployment frozen no-prune path shebang without with].each do |option|
remembered_flag_deprecation(option)
Expand Down Expand Up @@ -324,6 +325,7 @@ def install
method_option "strict", type: :boolean, banner: "Do not allow any gem to be updated past latest --patch | --minor | --major"
method_option "conservative", type: :boolean, banner: "Use bundle install conservative update behavior and do not allow shared dependencies to be updated."
method_option "all", type: :boolean, banner: "Update everything."
method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
def update(*gems)
require_relative "cli/update"
Bundler.settings.temporary(no_install: false) do
Expand Down Expand Up @@ -405,6 +407,7 @@ def binstubs(*gems)
method_option "skip-install", type: :boolean, banner: "Adds gem to the Gemfile but does not install it"
method_option "optimistic", type: :boolean, banner: "Adds optimistic declaration of version to gem"
method_option "strict", type: :boolean, banner: "Adds strict declaration of version to gem"
method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
def add(*gems)
require_relative "cli/add"
Add.new(options.dup, gems).run
Expand Down Expand Up @@ -435,6 +438,7 @@ def add(*gems)
method_option "filter-patch", type: :boolean, banner: "Only list patch newer versions"
method_option "parseable", aliases: "--porcelain", type: :boolean, banner: "Use minimal formatting for more parseable output"
method_option "only-explicit", type: :boolean, banner: "Only list gems specified in your Gemfile, not their dependencies"
method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable."
def outdated(*gems)
require_relative "cli/outdated"
Outdated.new(options, gems).run
Expand Down
3 changes: 3 additions & 0 deletions bundler/lib/bundler/cli/add.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ def initialize(options, gems)
def run
Bundler.ui.level = "warn" if options[:quiet]

Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]

validate_options!
inject_dependencies
perform_bundle_install unless options["skip-install"]
Expand Down
6 changes: 6 additions & 0 deletions bundler/lib/bundler/cli/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

module Bundler
module CLI::Common
def self.validate_cooldown!(value)
return if value.nil?
return if value.is_a?(Integer) && value >= 0
raise InvalidOption, "Expected `--cooldown` to be a non-negative integer, got #{value.inspect}"
end

def self.output_post_install_messages(messages)
return if Bundler.settings["ignore_messages"]
messages.to_a.each do |name, msg|
Expand Down
3 changes: 3 additions & 0 deletions bundler/lib/bundler/cli/install.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ def normalize_settings

Bundler.settings.set_command_option_if_given :jobs, options["jobs"]

Bundler::CLI::Common.validate_cooldown!(options["cooldown"])
Bundler.settings.set_command_option_if_given :cooldown, options["cooldown"]

Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"]

Bundler.settings.set_command_option_if_given :no_install, options["no-install"]
Expand Down
44 changes: 42 additions & 2 deletions bundler/lib/bundler/cli/outdated.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ def initialize(options, gems)
def run
check_for_deployment_mode!

Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]

Bundler.definition.validate_runtime!
current_specs = Bundler.ui.silence { Bundler.definition.resolve }

Expand Down Expand Up @@ -199,7 +202,15 @@ def print_gem(current_spec, active_spec, dependency, groups)
end

spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, " \
"installed #{current_version}#{dependency_version})"
"installed #{current_version}#{dependency_version}"

release_date = release_date_for(active_spec)
spec_outdated_info += ", released #{release_date}" unless release_date.empty?

remaining = cooldown_days_remaining(active_spec)
spec_outdated_info += ", in cooldown for #{remaining} more day#{"s" if remaining > 1}" if remaining

spec_outdated_info += ")"

output_message = if options[:parseable]
spec_outdated_info.to_s
Expand All @@ -215,13 +226,25 @@ def print_gem(current_spec, active_spec, dependency, groups)
def gem_column_for(current_spec, active_spec, dependency, groups)
current_version = "#{current_spec.version}#{current_spec.git_version}"
spec_version = "#{active_spec.version}#{active_spec.git_version}"
remaining = cooldown_days_remaining(active_spec)
spec_version += " (cooldown #{remaining}d)" if remaining
dependency = dependency.requirement if dependency

ret_val = [active_spec.name, current_version, spec_version, dependency.to_s, groups.to_s]
ret_val << release_date_for(active_spec)
ret_val << loaded_from_for(active_spec).to_s if Bundler.ui.debug?
ret_val
end

def cooldown_days_remaining(spec, now = Time.now)
return nil unless spec.respond_to?(:created_at) && spec.created_at
return nil unless spec.respond_to?(:remote) && spec.remote
days = spec.remote.effective_cooldown
return nil if days.nil? || days <= 0
remaining = days - ((now - spec.created_at) / 86_400.0)
remaining > 0 ? remaining.ceil : nil
end

def check_for_deployment_mode!
return unless Bundler.frozen_bundle?
suggested_command = if Bundler.settings.locations("frozen").keys.&([:global, :local]).any?
Expand Down Expand Up @@ -283,11 +306,28 @@ def print_indented(matrix)
end

def table_header
header = ["Gem", "Current", "Latest", "Requested", "Groups"]
header = ["Gem", "Current", "Latest", "Requested", "Groups", "Release Date"]
header << "Path" if Bundler.ui.debug?
header
end

def release_date_for(spec)
return "" unless spec.respond_to?(:date)

date = spec.date
return "" unless date

return "" unless Gem.const_defined?(:DEFAULT_SOURCE_DATE_EPOCH)
default_date = Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc
default_date = Time.utc(default_date.year, default_date.month, default_date.day)

date = date.utc if date.respond_to?(:utc)

return "" if date == default_date

date.strftime("%Y-%m-%d")
end

def justify(row, sizes)
row.each_with_index.map do |element, index|
element.ljust(sizes[index])
Expand Down
2 changes: 2 additions & 0 deletions bundler/lib/bundler/cli/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def run
opts["force"] = options[:redownload] if options[:redownload]

Bundler.settings.set_command_option_if_given :jobs, opts["jobs"]
Bundler::CLI::Common.validate_cooldown!(options[:cooldown])
Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown]

Bundler.definition.validate_runtime!

Expand Down
8 changes: 6 additions & 2 deletions bundler/lib/bundler/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ def source(source, *args, &blk)
options = args.last.is_a?(Hash) ? args.pop.dup : {}
options = normalize_hash(options)
source = normalize_source(source)
cooldown = options["cooldown"]
if cooldown && !(cooldown.is_a?(Integer) && cooldown >= 0)
raise InvalidOption, "Expected `cooldown` to be a non-negative integer, got #{cooldown.inspect}"
end

if options.key?("type")
options["type"] = options["type"].to_s
Expand All @@ -130,9 +134,9 @@ def source(source, *args, &blk)
source_opts = options.merge("uri" => source)
with_source(@sources.add_plugin_source(options["type"], source_opts), &blk)
elsif block_given?
with_source(@sources.add_rubygems_source("remotes" => source), &blk)
with_source(@sources.add_rubygems_source("remotes" => source, "cooldown" => cooldown), &blk)
else
@sources.add_global_rubygems_remote(source)
@sources.add_global_rubygems_remote(source, cooldown: cooldown)
end
end

Expand Down
12 changes: 11 additions & 1 deletion bundler/lib/bundler/endpoint_specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Bundler
class EndpointSpecification < Gem::Specification
include MatchRemoteMetadata

attr_reader :name, :version, :platform, :checksum
attr_reader :name, :version, :platform, :checksum, :created_at
attr_writer :dependencies
attr_accessor :remote, :locked_platform

Expand Down Expand Up @@ -145,6 +145,7 @@ def parse_metadata(data)
unless data
@required_ruby_version = nil
@required_rubygems_version = nil
@created_at = nil
return
end

Expand All @@ -161,6 +162,15 @@ def parse_metadata(data)
@required_rubygems_version = Gem::Requirement.new(v)
when "ruby"
@required_ruby_version = Gem::Requirement.new(v)
when "created_at"
value = v.is_a?(Array) ? v.last : v
if value.is_a?(String)
@created_at = begin
Time.new(value)
rescue ArgumentError
nil
end
end
end
end
rescue StandardError => e
Expand Down
5 changes: 5 additions & 0 deletions bundler/lib/bundler/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ def run(options)
Bundler.create_bundle_path

ProcessLock.lock do
# Invalidate any stale gem specification cache from before we acquired the lock.
# Another process may have installed gems while we were waiting.
Gem::Specification.reset
@definition.sources.clear_cache

@definition.ensure_equivalent_gemfile_and_lockfile(options[:deployment])

if @definition.dependencies.empty?
Expand Down
5 changes: 4 additions & 1 deletion bundler/lib/bundler/man/bundle-add.1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
.SH "NAME"
\fBbundle\-add\fR \- Add gem to the Gemfile and run bundle install
.SH "SYNOPSIS"
\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-quiet] [\-\-skip\-install] [\-\-strict|\-\-optimistic]
\fBbundle add\fR \fIGEM_NAME\fR [\-\-group=GROUP] [\-\-version=VERSION] [\-\-source=SOURCE] [\-\-path=PATH] [\-\-git=GIT|\-\-github=GITHUB] [\-\-branch=BRANCH] [\-\-ref=REF] [\-\-cooldown=NUMBER] [\-\-quiet] [\-\-skip\-install] [\-\-strict|\-\-optimistic]
.SH "DESCRIPTION"
Adds the named gem to the [\fBGemfile(5)\fR][Gemfile(5)] and run \fBbundle install\fR\. \fBbundle install\fR can be avoided by using the flag \fB\-\-skip\-install\fR\.
.SH "OPTIONS"
Expand Down Expand Up @@ -50,6 +50,9 @@ Adds optimistic declaration of version\.
.TP
\fB\-\-strict\fR
Adds strict declaration of version\.
.TP
\fB\-\-cooldown=<number>\fR
Only consider gem versions published at least \fInumber\fR days ago when resolving\. Pass \fB0\fR to disable cooldown for this run\. See \fBcooldown\fR in bundle\-config(1) for precedence rules\.
.SH "EXAMPLES"
.IP "1." 4
You can add the \fBrails\fR gem to the Gemfile without any version restriction\. The source of the gem will be the global source\.
Expand Down
7 changes: 6 additions & 1 deletion bundler/lib/bundler/man/bundle-add.1.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ bundle-add(1) -- Add gem to the Gemfile and run bundle install

`bundle add` <GEM_NAME> [--group=GROUP] [--version=VERSION] [--source=SOURCE]
[--path=PATH] [--git=GIT|--github=GITHUB] [--branch=BRANCH] [--ref=REF]
[--quiet] [--skip-install] [--strict|--optimistic]
[--cooldown=NUMBER] [--quiet] [--skip-install] [--strict|--optimistic]

## DESCRIPTION

Expand Down Expand Up @@ -56,6 +56,11 @@ Adds the named gem to the [`Gemfile(5)`][Gemfile(5)] and run `bundle install`.
* `--strict`:
Adds strict declaration of version.

* `--cooldown=<number>`:
Only consider gem versions published at least <number> days ago when
resolving. Pass `0` to disable cooldown for this run. See `cooldown`
in bundle-config(1) for precedence rules.

## EXAMPLES

1. You can add the `rails` gem to the Gemfile without any version restriction.
Expand Down
Loading
Loading