Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d1e1ac6
Added Gradle Wrapper support
gmazzo Aug 20, 2025
037bc89
Targeting smoke tests from fork
gmazzo Aug 24, 2025
8ec500d
Updated `DISTRIBUTION_URL_REGEX` with multiple literal
gmazzo Aug 24, 2025
d8d437a
Multi-directory support
gmazzo Aug 24, 2025
7c07184
Fixed broken smoke tests
gmazzo Aug 24, 2025
a8048f8
Put changes under `gradle_wrapper_updater` feature flag
gmazzo Aug 25, 2025
a6ac9ee
Fixed broken requirements logic when multiple wrappers
gmazzo Aug 25, 2025
aafaa8a
Renamed `lockfile_updater.rb` to `gradle_updater_base.rb`
gmazzo Aug 26, 2025
caaf009
Updating Gradle binaries support
gmazzo Aug 22, 2025
09a8761
Iterated distributions regex to catch any url
gmazzo Aug 30, 2025
06d21c8
Applied new RuboCop rules
gmazzo Sep 24, 2025
120892e
Merge branch 'main' into gradle-wrapper-support
kbukum1 Oct 21, 2025
77c8568
Merge branch 'main' into gradle-wrapper-support
kbukum1 Oct 28, 2025
8d34b8a
Allow repo variables to target forks on smoke tests
gmazzo Oct 28, 2025
d6fbfde
Reverted the changes on `smoke.yml` workflow
gmazzo Oct 28, 2025
9876c01
Merge branch 'main' into gradle-wrapper-support
gmazzo Oct 29, 2025
d676815
Added `released_at` field to Gradle Distribution versions
gmazzo Oct 29, 2025
39ad031
Reverted changes on `lockfile_updater.rb`
gmazzo Oct 29, 2025
71f0ceb
Unified `wrapper_updater.rb`
gmazzo Oct 29, 2025
d4259fb
Integrated available distributions into `package_details_fetcher.rb`
gmazzo Oct 30, 2025
52fdd02
Moved expected resolved versions to a fixture file
gmazzo Nov 4, 2025
5b97c70
Merge branch 'main' into gradle-wrapper-support
kbukum1 Nov 4, 2025
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
6 changes: 3 additions & 3 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:

env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SMOKE_TEST_BRANCH: main
SMOKE_TEST_BRANCH: ${{ vars.SMOKE_TEST_BRANCH || 'main' }}
jobs:
discover:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -44,7 +44,7 @@ jobs:
cat filtered.json

# Curl the smoke-test tests directory to get a list of tests to run
URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests?ref=${{ env.SMOKE_TEST_BRANCH }}
URL=https://api.github.com/repos/${{ vars.SMOKE_TEST_REPO || 'dependabot/smoke-tests' }}/contents/tests?ref=${{ env.SMOKE_TEST_BRANCH }}
curl $URL > tests.json

# Select the names that match smoke-$test*.yaml, where $test is the .text value from filtered.json
Expand Down Expand Up @@ -84,7 +84,7 @@ jobs:
- name: Download test
if: steps.cache-smoke-test.outputs.cache-hit != 'true'
run: |
URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/${{ matrix.suite.name }}?ref=${{ env.SMOKE_TEST_BRANCH }}
URL=https://api.github.com/repos/${{ vars.SMOKE_TEST_REPO || 'dependabot/smoke-tests' }}/contents/tests/${{ matrix.suite.name }}?ref=${{ env.SMOKE_TEST_BRANCH }}
curl $(gh api $URL --jq .download_url) -o smoke.yaml

- name: Cache Smoke Test
Expand Down
19 changes: 19 additions & 0 deletions gradle/lib/dependabot/gradle/distributions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# typed: strict
# frozen_string_literal: true

module Dependabot
module Gradle
module Distributions
extend T::Sig

DISTRIBUTION_DEPENDENCY_TYPE = "gradle-distribution"

sig { params(requirements: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Boolean) }
def self.distribution_requirements?(requirements)
requirements.any? do |req|
req.dig(:source, :type) == DISTRIBUTION_DEPENDENCY_TYPE
end
end
end
end
end
27 changes: 27 additions & 0 deletions gradle/lib/dependabot/gradle/file_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ class FileFetcher < Dependabot::FileFetchers::Base
SUPPORTED_SETTINGS_FILE_NAMES =
T.let(%w(settings.gradle settings.gradle.kts).freeze, T::Array[String])

SUPPORTED_WRAPPER_FILES_PATH = %w(
gradlew
gradlew.bat
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties
).freeze

# For now Gradle only supports library .toml files in the main gradle folder
SUPPORTED_VERSION_CATALOG_FILE_PATH =
T.let(%w(/gradle/libs.versions.toml).freeze, T::Array[String])
Expand Down Expand Up @@ -76,6 +83,7 @@ def fetch_files
def all_buildfiles_in_build(root_dir)
files = [buildfile(root_dir), settings_file(root_dir), version_catalog_file(root_dir), lockfile(root_dir)]
.compact
files += wrapper_files(root_dir)
files += subproject_buildfiles(root_dir)
files += subproject_lockfiles(root_dir)
files += dependency_script_plugins(root_dir)
Expand Down Expand Up @@ -172,6 +180,25 @@ def subproject_buildfiles(root_dir)
end
end

sig { params(dir: String).returns(T::Array[DependencyFile]) }
def wrapper_files(dir)
return [] unless Experiments.enabled?(:gradle_wrapper_updater)

SUPPORTED_WRAPPER_FILES_PATH.filter_map do |filename|
file = fetch_file_if_present(File.join(dir, filename))
next unless file

if file.name.end_with?(".jar")
file.content = Base64.encode64(T.must(file.content)) if file.content
file.content_encoding = DependencyFile::ContentEncoding::BASE64
end
file
rescue Dependabot::DependencyFileNotFound
# Gradle itself doesn't worry about missing subprojects, so we don't
nil
end
end

sig { params(root_dir: String).returns(T.nilable(DependencyFile)) }
def version_catalog_file(root_dir)
return nil unless root_dir == "."
Expand Down
20 changes: 20 additions & 0 deletions gradle/lib/dependabot/gradle/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class FileParser < Dependabot::FileParsers::Base # rubocop:disable Metrics/Class
extend T::Sig

require "dependabot/file_parsers/base/dependency_set"
require_relative "file_parser/distributions_finder"
require_relative "file_parser/property_value_finder"

SUPPORTED_BUILD_FILE_NAMES = T.let(
Expand Down Expand Up @@ -59,6 +60,9 @@ def parse
script_plugin_files.each do |plugin_file|
dependency_set += buildfile_dependencies(plugin_file)
end
wrapper_properties_file.each do |properties_file|
dependency_set += wrapper_properties_dependencies(properties_file)
end
version_catalog_file.each do |toml_file|
dependency_set += version_catalog_dependencies(toml_file)
end
Expand Down Expand Up @@ -119,6 +123,14 @@ def language
)
end

sig { params(properties_file: Dependabot::DependencyFile).returns(DependencySet) }
def wrapper_properties_dependencies(properties_file)
dependency_set = DependencySet.new
dependency = DistributionsFinder.resolve_dependency(properties_file)
dependency_set << dependency if dependency
dependency_set
end

sig { params(toml_file: Dependabot::DependencyFile).returns(DependencySet) }
def version_catalog_dependencies(toml_file)
dependency_set = DependencySet.new
Expand Down Expand Up @@ -544,6 +556,14 @@ def buildfiles
)
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def wrapper_properties_file
@wrapper_properties_file ||= T.let(
dependency_files.select { |f| f.name.end_with?("gradle-wrapper.properties") },
T.nilable(T::Array[Dependabot::DependencyFile])
)
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def version_catalog_file
@version_catalog_file ||= T.let(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# typed: strong
# frozen_string_literal: true

require "dependabot/gradle/file_parser"
require "dependabot/gradle/distributions"
require "sorbet-runtime"

module Dependabot
module Gradle
class FileParser
class DistributionsFinder
extend T::Sig

DISTRIBUTION_URL_REGEX =
/.*?(?<version>(\d+(?:\.\d+){1,3}(?:-(?!bin|all)\w++)*(?:\+\w++)*))(?:-bin|-all)?.*?/

sig { params(properties_file: DependencyFile).returns(T.nilable(Dependency)) }
def self.resolve_dependency(properties_file) # rubocop:disable Metrics/MethodLength
content = properties_file.content
return nil unless content

distribution_url, checksum = load_properties(content)
match = distribution_url&.match(DISTRIBUTION_URL_REGEX)&.named_captures
return nil unless match

version = match.fetch("version")

requirements = T.let(
[{
requirement: version,
file: properties_file.name,
source: {
type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
url: distribution_url,
property: "distributionUrl"
},
groups: []
}],
T::Array[T::Hash[Symbol, T.untyped]]
)

if checksum
requirements << {
requirement: checksum,
file: properties_file.name,
source: {
type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
url: "#{distribution_url}.sha256",
property: "distributionSha256Sum"
},
groups: []
}
end

Dependency.new(
name: "gradle-wrapper",
version: version,
requirements: requirements,
package_manager: "gradle"
)
end

sig { params(properties_content: String).returns(T::Array[T.nilable(String)]) }
def self.load_properties(properties_content)
distribution_url = T.let(nil, T.nilable(String))
checksum = T.let(nil, T.nilable(String))

properties_content.lines.each do |line|
(key, value) = line.split("=", 2).map(&:strip)
next unless key && value

case key
when "distributionUrl"
distribution_url = value.gsub("\\:", ":")
when "distributionSha256Sum"
checksum = value
else
next
end
break if distribution_url && checksum
end

[distribution_url, checksum]
end

private_class_method :load_properties
end
end
end
end
43 changes: 36 additions & 7 deletions gradle/lib/dependabot/gradle/file_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class FileUpdater < Dependabot::FileUpdaters::Base
require_relative "file_updater/dependency_set_updater"
require_relative "file_updater/property_value_updater"
require_relative "file_updater/lockfile_updater"
require_relative "file_updater/wrapper_updater"

SUPPORTED_BUILD_FILE_NAMES = %w(build.gradle build.gradle.kts gradle.lockfile).freeze

Expand Down Expand Up @@ -64,6 +65,10 @@ def original_file
def update_buildfiles_for_dependency(buildfiles:, dependency:)
files = buildfiles.dup

# dependencies may have multiple requirements targeting the same file or build dir
# we keep the last one by path to later run its native helpers
buildfiles_processed = T.let({}, T::Hash[String, Dependabot::DependencyFile])

# The UpdateChecker ensures the order of requirements is preserved
# when updating, so we can zip them together in new/old pairs.
reqs = dependency.requirements.zip(T.must(dependency.previous_requirements))
Expand Down Expand Up @@ -93,16 +98,20 @@ def update_buildfiles_for_dependency(buildfiles:, dependency:)
files[T.must(files.index(buildfile))] = update_version_in_buildfile(dependency, buildfile, old_req, new_req)
end

next unless Dependabot::Experiments.enabled?(:gradle_lockfile_updater)
buildfiles_processed[buildfile.name] = buildfile
end

# runs native updaters (e.g. wrapper, lockfile) on relevant build files updated
updaters = native_updaters(files, dependency)
buildfiles_processed.each do |_, buildfile|
updated_files = updaters.flat_map { |updater| updater.update_files(buildfile) }

lockfile_updater = LockfileUpdater.new(dependency_files: files)
lockfiles = lockfile_updater.update_lockfiles(buildfile)
lockfiles.each do |lockfile|
existing_file = files.find { |f| f.name == lockfile.name && f.directory == lockfile.directory }
updated_files.each do |file|
existing_file = files.find { |f| f.name == file.name && f.directory == file.directory }
if existing_file.nil?
files << lockfile
files << file
else
files[T.must(files.index(existing_file))] = lockfile
files[T.must(files.index(existing_file))] = file
end
end
end
Expand All @@ -113,6 +122,20 @@ def update_buildfiles_for_dependency(buildfiles:, dependency:)
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize

sig do
params(
files: T::Array[Dependabot::DependencyFile],
dependency: Dependabot::Dependency
).returns(T::Array[GradleUpdaterBase])
end
def native_updaters(files, dependency)
updaters = T.let([], T::Array[GradleUpdaterBase])
updaters << LockfileUpdater.new(dependency_files: files) if Experiments.enabled?(:gradle_lockfile_updater)
updaters << WrapperUpdater.new(dependency_files: files, dependency: dependency) if
Experiments.enabled?(:gradle_wrapper_updater)
updaters
end

sig do
params(
buildfiles: T::Array[Dependabot::DependencyFile],
Expand Down Expand Up @@ -191,6 +214,7 @@ def update_version_in_buildfile(
end

# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/PerceivedComplexity
sig do
params(
dependency: Dependabot::Dependency,
Expand All @@ -209,6 +233,10 @@ def original_buildfile_declarations(dependency, requirement)
if dependency.name.include?(":")
dep_parts = dependency.name.split(":")
next false unless line.include?(T.must(dep_parts.first)) || line.include?(T.must(dep_parts.last))
elsif T.let(requirement.fetch(:file), String).end_with?(".properties")
property = T.let(requirement, T::Hash[Symbol, T.nilable(T::Hash[Symbol, T.nilable(String)])])
.dig(:source, :property)
next false unless !property.nil? && line.start_with?(property)
elsif T.let(requirement.fetch(:file), String).end_with?(".toml")
next false unless line.include?(dependency.name)
else
Expand All @@ -221,6 +249,7 @@ def original_buildfile_declarations(dependency, requirement)
end
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/PerceivedComplexity

sig { params(string: String, buildfile: Dependabot::DependencyFile).returns(String) }
def evaluate_properties(string, buildfile)
Expand Down
Loading
Loading