Fetch custom Ruby version file specified by ruby file: option in Gemfile#14230
Fetch custom Ruby version file specified by ruby file: option in Gemfile#14230wt-l00 wants to merge 1 commit intodependabot:mainfrom
ruby file: option in Gemfile#14230Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds support for fetching custom Ruby version files specified via the ruby file: option in Gemfiles. This is a newly supported Gemfile syntax that allows specifying the Ruby version in arbitrary files (e.g., mise.toml) rather than just .ruby-version or .tool-versions. Without this feature, Dependabot cannot correctly interpret Ruby version constraints when this syntax is used, potentially causing dependency updates to fail.
Changes:
- Added file fetching support for custom ruby version files in the Bundler FileFetcher
- Added test fixtures for the new ruby_file_option project configuration
- Added test coverage across file_fetcher, file_parser/file_preparer, file_updater, and update_checker specs
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| bundler/lib/dependabot/bundler/file_fetcher.rb | Added ruby_file_version_file and ruby_file_version_filename methods to detect and fetch custom ruby version files from Gemfiles |
| bundler/spec/fixtures/projects/bundler2/ruby_file_option/Gemfile | Test fixture with ruby file: "custom-ruby-version" syntax |
| bundler/spec/fixtures/projects/bundler2/ruby_file_option/Gemfile.lock | Corresponding lockfile for ruby_file_option test project |
| bundler/spec/fixtures/projects/bundler2/ruby_file_option/custom-ruby-version | Custom version file containing "2.2.0" |
| bundler/spec/fixtures/github/gemfile_with_ruby_file_option_content.json | Mock GitHub API response for Gemfile with ruby file: option |
| bundler/spec/fixtures/github/custom_ruby_version_content.json | Mock GitHub API response for custom-ruby-version file |
| bundler/spec/fixtures/github/contents_ruby_with_custom_version_file.json | Mock GitHub API directory listing including custom version file |
| bundler/spec/dependabot/bundler/file_fetcher_spec.rb | Test case verifying custom version file is fetched |
| bundler/spec/dependabot/bundler/file_parser/file_preparer_spec.rb | Test case verifying custom version file content is prepared correctly |
| bundler/spec/dependabot/bundler/file_updater_spec.rb | Test case verifying updates work with custom version files |
| bundler/spec/dependabot/bundler/update_checker/latest_version_finder_spec.rb | Test case verifying version resolution works with custom version files |
| 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*['"]([^'"]+)['"]/) | ||
| match&.captures&.first | ||
| end |
There was a problem hiding this comment.
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.
| 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*['"]([^'"]+)['"]/) | ||
| match&.captures&.first | ||
| end |
There was a problem hiding this comment.
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.
| content = gemfile&.content | ||
| return unless content | ||
|
|
||
| match = content.match(/^\s*ruby\s+file:\s*['"]([^'"]+)['"]/) |
There was a problem hiding this comment.
The regex pattern could be made more robust to handle edge cases. Consider these improvements:
- The pattern should handle optional commas after the file: option (Gemfile syntax allows both "ruby file: 'x'" and "ruby file: 'x',")
- Consider supporting both old-style hash syntax (ruby :file => "x") and new-style (ruby file: "x")
- 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.
| match = content.match(/^\s*ruby\s+file:\s*['"]([^'"]+)['"]/) | |
| match = content.match(/^\s*ruby\s+(?:file:\s*|:file\s*=>\s*)['"]([^'"]+)['"]\s*,?.*/) |
| return unless content | ||
|
|
||
| match = content.match(/^\s*ruby\s+file:\s*['"]([^'"]+)['"]/) | ||
| match&.captures&.first |
There was a problem hiding this comment.
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?("\\")
filenameThis makes the security boundary explicit at the parsing level rather than relying solely on downstream sanitization.
| match&.captures&.first | |
| filename = match&.captures&.first | |
| return if filename&.include?("/") || filename&.include?("\\") | |
| filename |
| 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*['"]([^'"]+)['"]/) | ||
| match&.captures&.first | ||
| end |
There was a problem hiding this comment.
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.
…mfile Add support for Bundler's `ruby file: "filename"` syntax, which allows managing the Ruby version via an external file. The FileFetcher now fetches the referenced file as a support file. - Add `ruby_file_version_filename` to extract the filename from Gemfile content using a regex - Add `ruby_file_version_file` to fetch the file via `fetch_support_file` - Wire it into `fetch_files` following the same pattern as `.ruby-version` and `.tool-versions` ref: ruby/rubygems@fb9354b
ce61a43 to
0420775
Compare
What are you trying to accomplish?
Gemfile supports a ruby file: "custom-ruby-version" syntax to read the Ruby version from an arbitrary file (e.g. mise.toml). However, the Bundler file fetcher only fetched .ruby-version and .tool-versions as version constraint files, and did not fetch the custom version file specified by the ruby file: option.
Without fetching this file, Dependabot cannot correctly interpret Ruby version constraints, which may cause dependency updates to fail.
This PR fixes the issue by detecting the ruby file: option in the Gemfile and including the specified file in the set of fetched files.
Anything you want to highlight for special attention from reviewers?
(/^\s*ruby\s+file:\s*['"]([^'"]+)['"]/).tool-versions).
How will you know you've accomplished your goal?
The added spec verifies that when fetching files from a repository whose Gemfile contains ruby file: "custom-ruby-version", the specified custom version file is included in the fetched files, and the total number of fetched files is 3 (Gemfile, Gemfile.lock, and custom-ruby-version).
Checklist