Skip to content
Closed
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
23 changes: 23 additions & 0 deletions bundler/lib/dependabot/bundler/file_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def fetch_files
fetched_files += gemspecs
fetched_files << T.must(ruby_version_file) if ruby_version_file
fetched_files << T.must(tool_versions_file) if tool_versions_file
fetched_files << T.must(ruby_file_version_file) if ruby_file_version_file
fetched_files += path_gemspecs
fetched_files += find_included_files(fetched_files)

Expand Down Expand Up @@ -138,6 +139,28 @@ def tool_versions_file
@tool_versions_file ||= T.let(fetch_support_file(".tool-versions"), T.nilable(Dependabot::DependencyFile))
end

sig { returns(T.nilable(DependencyFile)) }
def ruby_file_version_file
return unless gemfile

@ruby_file_version_file ||= T.let(
begin
filename = ruby_file_version_filename
fetch_support_file(filename) if filename
end,
T.nilable(Dependabot::DependencyFile)
)
end

sig { returns(T.nilable(String)) }
def ruby_file_version_filename
content = gemfile&.content
return unless content

match = content.match(/^\s*ruby\s+file:\s*['"]([^'"]+)['"]/)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern could be made more robust to handle edge cases. Consider these improvements:

  1. The pattern should handle optional commas after the file: option (Gemfile syntax allows both "ruby file: 'x'" and "ruby file: 'x',")
  2. Consider supporting both old-style hash syntax (ruby :file => "x") and new-style (ruby file: "x")
  3. The pattern doesn't handle comments on the same line (e.g., "ruby file: 'x' # comment")

While the current pattern /^\sruby\s+file:\s'"['"]/ will work for basic cases, consider making it more flexible to match actual Gemfile parsing behavior. You could use a pattern like:

/^\sruby\s+(?:file:\s|:file\s*=>\s*)'"['"]/

This handles both hash syntaxes and is more aligned with Ruby conventions.

Suggested change
match = content.match(/^\s*ruby\s+file:\s*['"]([^'"]+)['"]/)
match = content.match(/^\s*ruby\s+(?:file:\s*|:file\s*=>\s*)['"]([^'"]+)['"]\s*,?.*/)

Copilot uses AI. Check for mistakes.
match&.captures&.first
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern allows any non-quote characters in the filename ([^'"]+), which could include path separators like "/" or "..". While the base FileFetcher class handles path sanitization via File.dirname/File.basename and .cleanpath, it would be more secure and clear to explicitly validate that the extracted filename doesn't contain path separators.

Consider adding validation after extracting the filename:

filename = match&.captures&.first
return if filename&.include?("/") || filename&.include?("\\")
filename

This makes the security boundary explicit at the parsing level rather than relying solely on downstream sanitization.

Suggested change
match&.captures&.first
filename = match&.captures&.first
return if filename&.include?("/") || filename&.include?("\\")
filename

Copilot uses AI. Check for mistakes.
end
Comment on lines +142 to +162
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file_preparer needs to be updated to include custom ruby version files in the prepared_dependency_files. Currently, the prepared_dependency_files method on lines 32-40 explicitly lists ruby_version_file and tool_versions_file, but does not include the newly fetched ruby_file_version_file.

Without this, even though the custom ruby version file is fetched by the FileFetcher, it won't be passed to the FileParser or FileUpdater, which will cause dependency resolution to fail when a Gemfile uses the "ruby file:" option.

Add a ruby_file_version_file method similar to ruby_version_file (lines 83-85) and tool_versions_file (lines 87-90), and include it in the array on line 32-40.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +162
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lockfile_updater needs to write custom ruby version files when creating temporary dependency files. Currently, write_temporary_dependency_files (lines 128-144) calls write_ruby_version_file and write_tool_versions_file to handle .ruby-version and .tool-versions files, but it doesn't handle custom ruby version files specified via the "ruby file:" option.

Add a write_ruby_file_version_file method similar to write_ruby_version_file (lines 147-153) and write_tool_versions_file (lines 156-162), and call it from write_temporary_dependency_files. Also add a ruby_file_version_file method similar to lines 212-214 and 217-219 to locate the custom version file from dependency_files.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +162
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The update_checker/file_preparer also needs to be updated to include custom ruby version files in the prepared_dependency_files. Currently, prepared_dependency_files on lines 118-124 includes ruby_version_file and tool_versions_file, but doesn't include the custom ruby version file specified via the "ruby file:" option.

Add a ruby_file_version_file method similar to ruby_version_file (lines 194-196) and tool_versions_file (lines 199-201), and include it in the array on lines 118-124.

Copilot uses AI. Check for mistakes.

sig { returns(T::Array[DependencyFile]) }
def path_gemspecs
gemspec_files = T.let([], T::Array[Dependabot::DependencyFile])
Expand Down
19 changes: 19 additions & 0 deletions bundler/lib/dependabot/bundler/file_parser/file_preparer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def prepared_dependency_files
lockfile,
ruby_version_file,
tool_versions_file,
ruby_file_version_file,
*imported_ruby_files,
*specification_files
].compact
Expand Down Expand Up @@ -89,6 +90,24 @@ def tool_versions_file
dependency_files.find { |f| f.name == ".tool-versions" }
end

# `ruby file: "..."` で指定されたカスタム Ruby バージョンファイルを返す
sig { returns(T.nilable(Dependabot::DependencyFile)) }
def ruby_file_version_file
filename = ruby_file_version_filename
return unless filename

dependency_files.find { |f| f.name == filename }
end

sig { returns(T.nilable(String)) }
def ruby_file_version_filename
content = gemfile&.content
return unless content

match = content.match(/^\s*ruby\s+file:\s*['"]([^'"]+)['"]/)
match&.captures&.first
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def imported_ruby_files
dependency_files
Expand Down
29 changes: 29 additions & 0 deletions bundler/lib/dependabot/bundler/file_updater/lockfile_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def write_temporary_dependency_files
write_gemspecs(top_level_gemspecs)
write_ruby_version_file
write_tool_versions_file
write_ruby_file_version_file
write_gemspecs(path_gemspecs)
write_specification_files
write_imported_ruby_files
Expand Down Expand Up @@ -161,6 +162,16 @@ def write_tool_versions_file
File.write(path, T.must(tool_versions_file).content)
end

# `ruby file: "..."` で指定されたカスタム Ruby バージョンファイルを一時ディレクトリに書き出す
sig { void }
def write_ruby_file_version_file
return unless ruby_file_version_file

path = T.must(ruby_file_version_file).name
FileUtils.mkdir_p(Pathname.new(path).dirname)
File.write(path, T.must(ruby_file_version_file).content)
end

sig { params(files: T::Array[Dependabot::DependencyFile]).void }
def write_gemspecs(files)
files.each do |file|
Expand Down Expand Up @@ -218,6 +229,24 @@ def tool_versions_file
dependency_files.find { |f| f.name == ".tool-versions" }
end

# `ruby file: "..."` で指定されたカスタム Ruby バージョンファイルを返す
sig { returns(T.nilable(Dependabot::DependencyFile)) }
def ruby_file_version_file
filename = ruby_file_version_filename
return unless filename

dependency_files.find { |f| f.name == filename }
end

sig { returns(T.nilable(String)) }
def ruby_file_version_filename
content = gemfile&.content
return unless content

match = content.match(/^\s*ruby\s+file:\s*['"]([^'"]+)['"]/)
match&.captures&.first
end

sig { params(lockfile_body: String).returns(String) }
def post_process_lockfile(lockfile_body)
lockfile_body = reorder_git_dependencies(lockfile_body)
Expand Down
21 changes: 20 additions & 1 deletion bundler/lib/dependabot/bundler/update_checker/file_preparer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,12 @@ def prepared_dependency_files
)
end

# No editing required for lockfile or Ruby version file
# lockfile Ruby バージョンファイルは編集不要のためそのまま追加
files += [
lockfile,
ruby_version_file,
tool_versions_file,
ruby_file_version_file,
*imported_ruby_files,
*specification_files
].compact
Expand Down Expand Up @@ -200,6 +201,24 @@ def tool_versions_file
dependency_files.find { |f| f.name == ".tool-versions" }
end

# `ruby file: "..."` で指定されたカスタム Ruby バージョンファイルを返す
sig { returns(T.nilable(Dependabot::DependencyFile)) }
def ruby_file_version_file
filename = ruby_file_version_filename
return unless filename

dependency_files.find { |f| f.name == filename }
end

sig { returns(T.nilable(String)) }
def ruby_file_version_filename
content = gemfile&.content
return unless content

match = content.match(/^\s*ruby\s+file:\s*['"]([^'"]+)['"]/)
match&.captures&.first
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def path_gemspecs
all = dependency_files.select { |f| f.name.end_with?(".gemspec") }
Expand Down
42 changes: 42 additions & 0 deletions bundler/spec/dependabot/bundler/file_fetcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,48 @@
end
end

context "with a custom version file referenced via `ruby file:` option" do
before do
stub_request(:get, url + "?ref=sha")
.with(headers: { "Authorization" => "token token" })
.to_return(
status: 200,
body: fixture("github", "contents_ruby_with_custom_version_file.json"),
headers: { "content-type" => "application/json" }
)

stub_request(:get, url + "Gemfile?ref=sha")
.with(headers: { "Authorization" => "token token" })
.to_return(
status: 200,
body: fixture("github", "gemfile_with_ruby_file_option_content.json"),
headers: { "content-type" => "application/json" }
)

stub_request(:get, url + "Gemfile.lock?ref=sha")
.with(headers: { "Authorization" => "token token" })
.to_return(
status: 200,
body: fixture("github", "gemfile_lock_content.json"),
headers: { "content-type" => "application/json" }
)

stub_request(:get, url + "custom-ruby-version?ref=sha")
.with(headers: { "Authorization" => "token token" })
.to_return(
status: 200,
body: fixture("github", "custom_ruby_version_content.json"),
headers: { "content-type" => "application/json" }
)
end

it "fetches the custom version file specified by `ruby file:` option" do
expect(file_fetcher_instance.files.count).to eq(3)
expect(file_fetcher_instance.files.map(&:name))
.to include("custom-ruby-version")
end
end

context "with a gems.rb rather than a Gemfile" do
before do
stub_request(:get, url + "?ref=sha")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@
its(:content) { is_expected.to eq("ruby 2.2.0\n") }
end

describe "the updated custom ruby version file specified by `ruby file:` option" do
subject do
prepared_dependency_files.find { |f| f.name == "custom-ruby-version" }
end

let(:dependency_files) { bundler_project_dependency_files("ruby_file_option") }

its(:content) { is_expected.to eq("2.2.0\n") }
end

describe "the updated .specification file" do
subject do
prepared_dependency_files.find { |f| f.name == "plugins/example/.specification" }
Expand Down
18 changes: 18 additions & 0 deletions bundler/spec/dependabot/bundler/file_updater_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,24 @@
end
end

context "when the Gemfile loads a custom ruby version file via `ruby file:` option" do
let(:project_name) { "ruby_file_option" }
let(:updater) do
described_class.new(
dependency_files: dependency_files,
dependencies: [dependency],
credentials: [{
"type" => "git_source",
"host" => "github.com"
}]
)
end

it "locks the updated gem to the latest version" do
expect(file.content).to include "business (1.5.0)"
end
end

context "when the Gemfile.lock didn't have a BUNDLED WITH line" do
let(:project_name) { "no_bundled_with" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@
its([:version]) { is_expected.to eq(Dependabot::Bundler::Version.new("1.5.0")) }
end

context "when the Gemfile loads a custom ruby version file via `ruby file:` option" do
let(:dependency_files) { bundler_project_dependency_files("ruby_file_option") }

its([:version]) { is_expected.to eq(Dependabot::Bundler::Version.new("1.5.0")) }
end

context "with a gemspec and a Gemfile" do
let(:dependency_files) { bundler_project_dependency_files("gemfile_small_example") }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[
{
"name": "Gemfile",
"path": "Gemfile",
"sha": "88b4e0a1c8093fae2b4fa52534035f9f85ed0956",
"size": 100,
"url": "https://api.github.com/repos/gocardless/bump/contents/Gemfile?ref=master",
"html_url": "https://github.com/gocardless/bump/blob/master/Gemfile",
"git_url": "https://api.github.com/repos/gocardless/bump/git/blobs/88b4e0a1c8093fae2b4fa52534035f9f85ed0956",
"download_url": "https://raw.githubusercontent.com/gocardless/bump/master/Gemfile",
"type": "file",
"_links": {
"self": "https://api.github.com/repos/gocardless/bump/contents/Gemfile?ref=master",
"git": "https://api.github.com/repos/gocardless/bump/git/blobs/88b4e0a1c8093fae2b4fa52534035f9f85ed0956",
"html": "https://github.com/gocardless/bump/blob/master/Gemfile"
}
},
{
"name": "Gemfile.lock",
"path": "Gemfile.lock",
"sha": "d429264c8c2f0f306a422900c2f41123e07c31b4",
"size": 100,
"url": "https://api.github.com/repos/gocardless/bump/contents/Gemfile.lock?ref=master",
"html_url": "https://github.com/gocardless/bump/blob/master/Gemfile.lock",
"git_url": "https://api.github.com/repos/gocardless/bump/git/blobs/d429264c8c2f0f306a422900c2f41123e07c31b4",
"download_url": "https://raw.githubusercontent.com/gocardless/bump/master/Gemfile.lock",
"type": "file",
"_links": {
"self": "https://api.github.com/repos/gocardless/bump/contents/Gemfile.lock?ref=master",
"git": "https://api.github.com/repos/gocardless/bump/git/blobs/d429264c8c2f0f306a422900c2f41123e07c31b4",
"html": "https://github.com/gocardless/bump/blob/master/Gemfile.lock"
}
},
{
"name": "custom-ruby-version",
"path": "custom-ruby-version",
"sha": "005119baaa0653ca59d923010341d8341daa8c43",
"size": 6,
"url": "https://api.github.com/repos/gocardless/bump/contents/custom-ruby-version?ref=master",
"html_url": "https://github.com/gocardless/bump/blob/master/custom-ruby-version",
"git_url": "https://api.github.com/repos/gocardless/bump/git/blobs/005119baaa0653ca59d923010341d8341daa8c43",
"download_url": "https://raw.githubusercontent.com/gocardless/bump/master/custom-ruby-version",
"type": "file",
"_links": {
"self": "https://api.github.com/repos/gocardless/bump/contents/custom-ruby-version?ref=master",
"git": "https://api.github.com/repos/gocardless/bump/git/blobs/005119baaa0653ca59d923010341d8341daa8c43",
"html": "https://github.com/gocardless/bump/blob/master/custom-ruby-version"
}
}
]
18 changes: 18 additions & 0 deletions bundler/spec/fixtures/github/custom_ruby_version_content.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "custom-ruby-version",
"path": "custom-ruby-version",
"sha": "005119baaa0653ca59d923010341d8341daa8c43",
"size": 6,
"url": "https://api.github.com/repos/gocardless/bump/contents/custom-ruby-version?ref=master",
"html_url": "https://github.com/gocardless/bump/blob/master/custom-ruby-version",
"git_url": "https://api.github.com/repos/gocardless/bump/git/blobs/005119baaa0653ca59d923010341d8341daa8c43",
"download_url": "https://raw.githubusercontent.com/gocardless/bump/master/custom-ruby-version",
"type": "file",
"content": "My4yLjAK\n",
"encoding": "base64",
"_links": {
"self": "https://api.github.com/repos/gocardless/bump/contents/custom-ruby-version?ref=master",
"git": "https://api.github.com/repos/gocardless/bump/git/blobs/005119baaa0653ca59d923010341d8341daa8c43",
"html": "https://github.com/gocardless/bump/blob/master/custom-ruby-version"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "Gemfile",
"path": "Gemfile",
"sha": "aabbcc1122334455667788990011223344556677",
"size": 80,
"url": "https://api.github.com/repos/gocardless/bump/contents/Gemfile?ref=master",
"html_url": "https://github.com/gocardless/bump/blob/master/Gemfile",
"git_url": "https://api.github.com/repos/gocardless/bump/git/blobs/aabbcc1122334455667788990011223344556677",
"download_url": "https://raw.githubusercontent.com/gocardless/bump/master/Gemfile",
"type": "file",
"content": "c291cmNlICJodHRwczovL3J1YnlnZW1zLm9yZyIKCnJ1YnkgZmlsZTogImN1\nc3RvbS1ydWJ5LXZlcnNpb24iCgpnZW0gImJ1c2luZXNzIiwgIn4+IDEuMCIK\n",
"encoding": "base64",
"_links": {
"self": "https://api.github.com/repos/gocardless/bump/contents/Gemfile?ref=master",
"git": "https://api.github.com/repos/gocardless/bump/git/blobs/aabbcc1122334455667788990011223344556677",
"html": "https://github.com/gocardless/bump/blob/master/Gemfile"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source "https://rubygems.org"

ruby file: "custom-ruby-version"

gem "business", "~> 1.4.0"
gem "statesman", "~> 1.2.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
GEM
remote: https://rubygems.org/
specs:
business (1.4.0)
statesman (1.2.1)

PLATFORMS
ruby

DEPENDENCIES
business (~> 1.4.0)
statesman (~> 1.2.0)

BUNDLED WITH
2.2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.2.0
Loading