dependabot-gradle 0.382.0 → 0.383.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a3b9947c1607a8ada6ef1d74f34deb403f2cf76196658ba15b0390976f96c23
4
- data.tar.gz: f1471d46f94ed018c4ddfa41c90ab67333fc5a562f3db9e1d9b1a7145964c3bf
3
+ metadata.gz: d3cb4e728e6f95cd0ba68227af68fd31b0e30ff55a7c28090bfd7f9bc0a357bd
4
+ data.tar.gz: 268f0b46c9fce142acc4f4b615803481e6e9a72844aafcaee0b18becccd471ef
5
5
  SHA512:
6
- metadata.gz: 7d3a0d1e02c8fda4cb95d43834bce4d09e7eb591c8b2690e6e7e8ec2d40cb0c2128917fe129ee7d9c3683a4a5e6a5498351540430074b948bad4b9fd6c0a0fc1
7
- data.tar.gz: d166b1faeacb7545f0c5327d68209fbbc980fce1a94ad9e6233dd227590ad8a0c8f1fae90380259ba60991850ba274cf6b468470bed59eeebe44085841921619
6
+ metadata.gz: 935e89f7162570265dc615315f89267fa5aee57d231e5ad7f561d4f08e7759310b3deff9c2e9170c8286d5e57384574c990bfc6be3101835e5ab72d5cf5b1a63
7
+ data.tar.gz: 06c419e20a01912105e76ec70577a27ab9fc091b37493b7d2481ae7e3383fc1d971004b6561b573f45db9bb0d54cfaafa35588e38b5e1aafd8f5c50cbc31f563
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "dependabot/dependency_requirement"
5
+
4
6
  module Dependabot
5
7
  module Gradle
6
8
  module Distributions
@@ -9,7 +11,7 @@ module Dependabot
9
11
  DISTRIBUTION_REPOSITORY_URL = "https://services.gradle.org"
10
12
  DISTRIBUTION_DEPENDENCY_TYPE = "gradle-distribution"
11
13
 
12
- sig { params(requirements: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Boolean) }
14
+ sig { params(requirements: T::Array[Dependabot::DependencyRequirement]).returns(T::Boolean) }
13
15
  def self.distribution_requirements?(requirements)
14
16
  requirements.any? do |req|
15
17
  req.dig(:source, :type) == DISTRIBUTION_DEPENDENCY_TYPE
@@ -0,0 +1,103 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/gradle/file_updater"
7
+ require "dependabot/gradle/file_updater/wrapper/gradle_version_capabilities"
8
+ require "dependabot/gradle/file_updater/wrapper/properties_document"
9
+
10
+ module Dependabot
11
+ module Gradle
12
+ class FileUpdater
13
+ module Wrapper
14
+ # Builds the argument list for Gradle's `wrapper` task.
15
+ #
16
+ # The output file is reconciled afterwards (see PropertiesReconciler), so these arguments do
17
+ # not need to fully reproduce the user's file. We still forward the user's gated, run-relevant
18
+ # settings (networkTimeout/retries/retryBackOffMs) when the *executing* Gradle version supports
19
+ # them, both to honor the user's configuration during the run and to align with the wrapper
20
+ # task's intended usage. Options unsupported by the executing version are omitted so the run
21
+ # never aborts on an unknown flag.
22
+ class CommandBuilder
23
+ extend T::Sig
24
+
25
+ # Maps an existing properties key to its wrapper CLI option and the capability key used to
26
+ # decide whether the executing Gradle version understands the flag.
27
+ STEERED_OPTIONS = T.let(
28
+ [
29
+ ["networkTimeout", "--network-timeout", GradleVersionCapabilities::NETWORK_TIMEOUT],
30
+ ["retries", "--retries", GradleVersionCapabilities::RETRIES],
31
+ ["retryBackOffMs", "--retry-back-off-ms", GradleVersionCapabilities::RETRY_BACK_OFF_MS]
32
+ ].freeze,
33
+ T::Array[[String, String, String]]
34
+ )
35
+
36
+ sig do
37
+ params(
38
+ requirements: T::Array[Dependabot::DependencyRequirement],
39
+ original_properties: T.nilable(PropertiesDocument),
40
+ gradle_version: T.nilable(Dependabot::Gradle::Version)
41
+ ).void
42
+ end
43
+ def initialize(requirements:, original_properties:, gradle_version:)
44
+ @requirements = requirements
45
+ @original_properties = original_properties
46
+ @gradle_version = gradle_version
47
+ end
48
+
49
+ sig { returns(T::Array[String]) }
50
+ def build
51
+ args = %W(wrapper --gradle-version #{version})
52
+
53
+ # Dependabot's proxy cannot satisfy the HEAD request Gradle issues to validate the
54
+ # distribution URL, so we always skip validation. The user's original
55
+ # `validateDistributionUrl` value is preserved by reconciliation.
56
+ args += %w(--no-validate-url)
57
+
58
+ args += steered_args
59
+ args += %W(--distribution-type #{distribution_type}) if distribution_type
60
+ args += %W(--gradle-distribution-sha256-sum #{checksum}) if checksum
61
+ args
62
+ end
63
+
64
+ private
65
+
66
+ sig { returns(T::Array[String]) }
67
+ def steered_args
68
+ properties = @original_properties
69
+ return [] if properties.nil?
70
+
71
+ STEERED_OPTIONS.flat_map do |property_key, flag, capability|
72
+ value = properties.value_for(property_key)
73
+ next [] if value.nil? || value.strip.empty?
74
+ next [] unless GradleVersionCapabilities.supports?(capability, @gradle_version)
75
+
76
+ [flag, value]
77
+ end
78
+ end
79
+
80
+ sig { returns(String) }
81
+ def version
82
+ T.let(T.must(@requirements[0])[:requirement], String)
83
+ end
84
+
85
+ sig { returns(T.nilable(String)) }
86
+ def checksum
87
+ return nil unless @requirements.size > 1
88
+
89
+ T.let(T.must(@requirements[1])[:requirement], String)
90
+ end
91
+
92
+ sig { returns(T.nilable(String)) }
93
+ def distribution_type
94
+ url = T.let(T.must(@requirements[0])[:source], T::Hash[Symbol, String])[:url]
95
+ # Anchor to the `-bin.zip` / `-all.zip` filename suffix so a path segment such as a
96
+ # mirror host (e.g. https://binaries.example.com/...) can't false-match `bin`/`all`.
97
+ url&.match(/-(bin|all)\.zip/)&.captures&.first
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,60 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/gradle/file_updater"
7
+ require "dependabot/gradle/version"
8
+
9
+ module Dependabot
10
+ module Gradle
11
+ class FileUpdater
12
+ module Wrapper
13
+ # Detects the Gradle version that will actually *execute* the wrapper task.
14
+ #
15
+ # The wrapper task runs under whatever Gradle the project currently resolves to (the OLD
16
+ # distribution), not the target version. Knowing that version lets CommandBuilder decide
17
+ # which version-gated CLI flags are safe to pass.
18
+ module ExecutingVersionDetector
19
+ extend T::Sig
20
+
21
+ # Matches the version embedded in a Gradle distribution URL, e.g.
22
+ # https://services.gradle.org/distributions/gradle-9.5.0-bin.zip
23
+ # Anchored to the `gradle-<version>-(bin|all).zip` filename so host/port numbers in custom
24
+ # mirror URLs are never mistaken for the version. The captured token is validated with
25
+ # Version.correct? so non-version matches (and RC/milestone names) are handled safely.
26
+ DISTRIBUTION_URL_VERSION_REGEX = T.let(
27
+ /gradle-(?<version>.+?)-(?:bin|all)\.zip/,
28
+ Regexp
29
+ )
30
+
31
+ # Matches the version printed by `gradle --version`, e.g. "Gradle 9.2.1".
32
+ GRADLE_VERSION_OUTPUT_REGEX = T.let(/^Gradle\s+(?<version>\d+(?:\.\d+){1,2}(?:-[\w.]+)?)/, Regexp)
33
+
34
+ sig { params(distribution_url: T.nilable(String)).returns(T.nilable(Dependabot::Gradle::Version)) }
35
+ def self.from_distribution_url(distribution_url)
36
+ return nil if distribution_url.nil?
37
+
38
+ captured = distribution_url.match(DISTRIBUTION_URL_VERSION_REGEX)&.named_captures&.fetch("version", nil)
39
+ build_version(captured)
40
+ end
41
+
42
+ sig { params(output: T.nilable(String)).returns(T.nilable(Dependabot::Gradle::Version)) }
43
+ def self.from_version_output(output)
44
+ return nil if output.nil?
45
+
46
+ captured = output.match(GRADLE_VERSION_OUTPUT_REGEX)&.named_captures&.fetch("version", nil)
47
+ build_version(captured)
48
+ end
49
+
50
+ sig { params(captured: T.nilable(String)).returns(T.nilable(Dependabot::Gradle::Version)) }
51
+ def self.build_version(captured)
52
+ return nil if captured.nil? || !Dependabot::Gradle::Version.correct?(captured)
53
+
54
+ T.cast(Dependabot::Gradle::Version.new(captured), Dependabot::Gradle::Version)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,59 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/gradle/file_updater"
7
+ require "dependabot/gradle/version"
8
+
9
+ module Dependabot
10
+ module Gradle
11
+ class FileUpdater
12
+ module Wrapper
13
+ # Declarative table of `wrapper` task command-line options and the minimum Gradle
14
+ # version that supports each one. The wrapper task is executed by the Gradle version
15
+ # currently resolved by the project (not the target version), so passing an option
16
+ # that the executing Gradle does not understand would abort the run. We use this table
17
+ # to only emit options that are safe for the executing version.
18
+ #
19
+ # Sources (gradle/gradle `Wrapper.java`):
20
+ # --network-timeout @since 7.6
21
+ # --validate-url @since 8.2 (incubating)
22
+ # --retries @since 9.5.0 (incubating)
23
+ # --retry-back-off-ms @since 9.5.0 (incubating)
24
+ module GradleVersionCapabilities
25
+ extend T::Sig
26
+
27
+ NETWORK_TIMEOUT = "network-timeout"
28
+ VALIDATE_URL = "validate-url"
29
+ RETRIES = "retries"
30
+ RETRY_BACK_OFF_MS = "retry-back-off-ms"
31
+
32
+ MINIMUM_VERSIONS = T.let(
33
+ {
34
+ NETWORK_TIMEOUT => "7.6",
35
+ VALIDATE_URL => "8.2",
36
+ RETRIES => "9.5.0",
37
+ RETRY_BACK_OFF_MS => "9.5.0"
38
+ }.freeze,
39
+ T::Hash[String, String]
40
+ )
41
+
42
+ # Returns true when the given (executing) Gradle version is known to support the option.
43
+ # When the version is unknown (nil) we conservatively refuse options that are gated behind
44
+ # a minimum version, so we never pass a flag that could abort the wrapper run. Reconciliation
45
+ # of the properties file guarantees the user's value is preserved regardless.
46
+ sig { params(option: String, gradle_version: T.nilable(Dependabot::Gradle::Version)).returns(T::Boolean) }
47
+ def self.supports?(option, gradle_version)
48
+ minimum = MINIMUM_VERSIONS[option]
49
+ return true if minimum.nil? # ungated option
50
+
51
+ return false if gradle_version.nil?
52
+
53
+ gradle_version >= Dependabot::Gradle::Version.new(minimum)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,96 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/gradle/file_updater"
7
+
8
+ module Dependabot
9
+ module Gradle
10
+ class FileUpdater
11
+ module Wrapper
12
+ # An order-preserving model of a `gradle-wrapper.properties` file.
13
+ #
14
+ # Java's `Properties.store` (used by Gradle's wrapper task) discards comments, blank
15
+ # lines, custom keys and the original key ordering. To preserve everything the user
16
+ # configured, we parse the original file into this document, mutate only the keys we
17
+ # intend to change, and render it back verbatim.
18
+ class PropertiesDocument
19
+ extend T::Sig
20
+
21
+ # A single physical line: either a raw line (comment/blank/unparsed) or a property.
22
+ class Line < T::Struct
23
+ prop :raw, String
24
+ prop :indent, String, default: ""
25
+ prop :key, T.nilable(String)
26
+ prop :separator, T.nilable(String)
27
+ prop :value, T.nilable(String)
28
+ end
29
+
30
+ # Properties files use `=`, `:` or whitespace as the key/value separator. Gradle always
31
+ # writes `=`, but we parse all three so user-authored files are handled faithfully.
32
+ KEY_VALUE_REGEX = T.let(/\A(\s*)([^\s:=]+)(\s*[:=]\s*|\s+)(.*)\z/, Regexp)
33
+ COMMENT_REGEX = T.let(/\A\s*[#!]/, Regexp)
34
+
35
+ sig { params(content: String).returns(PropertiesDocument) }
36
+ def self.parse(content)
37
+ lines = content.split("\n", -1).map { |line| parse_line(line) }
38
+ new(lines)
39
+ end
40
+
41
+ sig { params(line: String).returns(Line) }
42
+ def self.parse_line(line)
43
+ return Line.new(raw: line) if line.strip.empty? || line.match?(COMMENT_REGEX)
44
+
45
+ match = line.match(KEY_VALUE_REGEX)
46
+ return Line.new(raw: line) unless match
47
+
48
+ Line.new(
49
+ raw: line,
50
+ indent: match[1] || "",
51
+ key: match[2],
52
+ separator: match[3],
53
+ value: match[4]
54
+ )
55
+ end
56
+
57
+ sig { params(lines: T::Array[Line]).void }
58
+ def initialize(lines)
59
+ @lines = lines
60
+ end
61
+
62
+ sig { params(key: String).returns(T::Boolean) }
63
+ def key?(key)
64
+ @lines.any? { |line| line.key == key }
65
+ end
66
+
67
+ sig { params(key: String).returns(T.nilable(String)) }
68
+ def value_for(key)
69
+ @lines.find { |line| line.key == key }&.value
70
+ end
71
+
72
+ # Sets `key` to `value`, preserving the line's original position, indentation and separator
73
+ # when the key already exists, otherwise appending a new `key=value` line at the end.
74
+ sig { params(key: String, value: String).void }
75
+ def upsert(key, value)
76
+ existing = @lines.find { |line| line.key == key }
77
+ if existing
78
+ separator = existing.separator || "="
79
+ existing.value = value
80
+ existing.separator = separator
81
+ existing.raw = "#{existing.indent}#{key}#{separator}#{value}"
82
+ return
83
+ end
84
+
85
+ @lines << Line.new(raw: "#{key}=#{value}", key: key, separator: "=", value: value)
86
+ end
87
+
88
+ sig { returns(String) }
89
+ def to_s
90
+ @lines.map(&:raw).join("\n")
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,71 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/gradle/file_updater"
7
+ require "dependabot/gradle/file_updater/wrapper/properties_document"
8
+
9
+ module Dependabot
10
+ module Gradle
11
+ class FileUpdater
12
+ module Wrapper
13
+ # Reconciles the `gradle-wrapper.properties` file after Gradle's wrapper task has
14
+ # regenerated it from hardcoded defaults (see https://github.com/gradle/gradle/issues/36172).
15
+ #
16
+ # The reconciliation policy is deliberately conservative: the user's original file is the
17
+ # source of truth for everything (comments, ordering, custom keys, networkTimeout, retries,
18
+ # retryBackOffMs, validateDistributionUrl, distributionBase/Path, store paths, ...). Only the
19
+ # keys that legitimately change for a version bump are taken from the regenerated file.
20
+ class PropertiesReconciler
21
+ extend T::Sig
22
+
23
+ # Keys whose value is owned by the update itself and therefore taken from the regenerated
24
+ # file. Everything not listed here is preserved verbatim from the user's original file.
25
+ MANAGED_KEYS = T.let(
26
+ %w(distributionUrl distributionSha256Sum).freeze,
27
+ T::Array[String]
28
+ )
29
+
30
+ # Reconciles the regenerated wrapper properties back onto the user's original file.
31
+ #
32
+ # When the original file is missing there is nothing to preserve, so nil is returned and the
33
+ # caller keeps the regenerated file as-is. When only the regenerated file is missing (e.g. the
34
+ # wrapper task did not produce one) the original content is returned unchanged.
35
+ sig do
36
+ params(original_content: T.nilable(String), regenerated_content: T.nilable(String))
37
+ .returns(T.nilable(String))
38
+ end
39
+ def self.reconcile(original_content:, regenerated_content:)
40
+ new(original_content: original_content, regenerated_content: regenerated_content).reconcile
41
+ end
42
+
43
+ sig { params(original_content: T.nilable(String), regenerated_content: T.nilable(String)).void }
44
+ def initialize(original_content:, regenerated_content:)
45
+ @original_content = original_content
46
+ @regenerated_content = regenerated_content
47
+ end
48
+
49
+ sig { returns(T.nilable(String)) }
50
+ def reconcile
51
+ original_content = @original_content
52
+ regenerated_content = @regenerated_content
53
+ return original_content if original_content.nil? || regenerated_content.nil?
54
+
55
+ document = PropertiesDocument.parse(original_content)
56
+ regenerated = PropertiesDocument.parse(regenerated_content)
57
+
58
+ MANAGED_KEYS.each do |key|
59
+ new_value = regenerated.value_for(key)
60
+ next if new_value.nil?
61
+
62
+ document.upsert(key, new_value)
63
+ end
64
+
65
+ document.to_s
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -7,6 +7,7 @@ require "shellwords"
7
7
  require "pathname"
8
8
 
9
9
  require "dependabot/gradle/distributions"
10
+ require "dependabot/gradle/version"
10
11
 
11
12
  module Dependabot
12
13
  module Gradle
@@ -15,6 +16,11 @@ module Dependabot
15
16
  extend T::Sig
16
17
  include Dependabot::Gradle::Distributions
17
18
 
19
+ require_relative "wrapper/command_builder"
20
+ require_relative "wrapper/executing_version_detector"
21
+ require_relative "wrapper/properties_document"
22
+ require_relative "wrapper/properties_reconciler"
23
+
18
24
  sig { params(dependency_files: T::Array[Dependabot::DependencyFile], dependency: Dependabot::Dependency).void }
19
25
  def initialize(dependency_files:, dependency:)
20
26
  @dependency_files = dependency_files
@@ -68,14 +74,11 @@ module Dependabot
68
74
  FileUtils.chmod("+x", "./gradlew") if has_local_script
69
75
 
70
76
  properties_file = File.join(cwd, "gradle/wrapper/gradle-wrapper.properties")
71
- validate_distribution, network_timeout = read_wrapper_options(properties_file)
77
+ original_properties_content = read_file(properties_file)
78
+ original_document = original_properties_content && Wrapper::PropertiesDocument.parse(original_properties_content)
72
79
  env = { "JAVA_OPTS" => proxy_args.join(" ") } # set proxy for gradle execution
73
80
 
74
- command_parts = %w(--no-daemon --stacktrace) + command_args(target_requirements, network_timeout)
75
-
76
- # There is no guarantee that the `gradlew` script is present on the project,
77
- # if it's not, we fall back to system Gradle
78
- command = Shellwords.join([has_local_script ? "./gradlew" : "gradle"] + command_parts)
81
+ command = local_wrapper_command(has_local_script, target_requirements, original_document, cwd, env)
79
82
 
80
83
  begin
81
84
  # first attempt: run the wrapper task via the local Gradle wrapper (if present)
@@ -83,18 +86,23 @@ module Dependabot
83
86
  # the `gradlew` script may be corrupted, so we try and fall back to system Gradle before giving up
84
87
  SharedHelpers.run_shell_command(command, cwd: cwd, env: env)
85
88
  rescue SharedHelpers::HelperSubprocessFailed => e
86
- raise e unless has_local_script # already field with system one, there is no point to retry
89
+ raise e unless has_local_script # already failed with system one, there is no point to retry
87
90
 
88
91
  Dependabot.logger.warn("Running #{command} failed, retrying first with system Gradle: #{e.message}")
89
92
 
90
- # second attempt: run the wrapper task via system gradle and then retry via local wrapper
91
- system_command = Shellwords.join(["gradle"] + command_parts)
93
+ # second attempt: run the wrapper task via system gradle and then retry via local wrapper.
94
+ # The system Gradle may be a different (often older) version than the wrapper, so we rebuild
95
+ # the command against its detected version to avoid passing unsupported, version-gated flags.
96
+ system_command = system_wrapper_command(target_requirements, original_document, cwd, env)
92
97
  SharedHelpers.run_shell_command(system_command, cwd: cwd, env: env) # run via system gradle
93
98
  SharedHelpers.run_shell_command(command, cwd: cwd, env: env) # retry via local wrapper
94
99
  end
95
100
 
96
- # Restore previous validateDistributionUrl option if it existed
97
- override_validate_distribution_url_option(properties_file, validate_distribution)
101
+ # Gradle's wrapper task regenerates gradle-wrapper.properties from hardcoded defaults
102
+ # (https://github.com/gradle/gradle/issues/36172), discarding comments, ordering, custom
103
+ # keys and user-customized values. Reconcile the regenerated file back onto the user's
104
+ # original so only the version-bump keys change.
105
+ reconcile_properties(properties_file, original_properties_content)
98
106
 
99
107
  update_files_content(temp_dir, local_files, updated_files)
100
108
  rescue SharedHelpers::HelperSubprocessFailed => e
@@ -149,55 +157,68 @@ module Dependabot
149
157
  Pathname.new(file.name).cleanpath.to_path
150
158
  end
151
159
 
152
- # rubocop:disable Metrics/PerceivedComplexity
160
+ # Builds the command for the first wrapper attempt.
161
+ #
162
+ # When the project ships a `gradlew` script we run it: it downloads and executes the Gradle
163
+ # version pinned in the current gradle-wrapper.properties, so wrapper flags are gated on that
164
+ # distributionUrl version. When `gradlew` is missing we instead invoke system Gradle directly,
165
+ # so flags must be gated on the system Gradle's detected version (not the wrapper's) to avoid
166
+ # forwarding options that an older system Gradle does not understand and aborting the run.
167
+ sig do
168
+ params(
169
+ has_local_script: T::Boolean,
170
+ requirements: T::Array[Dependabot::DependencyRequirement],
171
+ original_document: T.nilable(Wrapper::PropertiesDocument),
172
+ cwd: String,
173
+ env: T::Hash[String, String]
174
+ ).returns(String)
175
+ end
176
+ def local_wrapper_command(has_local_script, requirements, original_document, cwd, env)
177
+ return system_wrapper_command(requirements, original_document, cwd, env) unless has_local_script
178
+
179
+ distribution_url = original_document&.value_for("distributionUrl")
180
+ gradle_version = Wrapper::ExecutingVersionDetector.from_distribution_url(distribution_url)
181
+ Shellwords.join(["./gradlew"] + build_command_parts(requirements, original_document, gradle_version))
182
+ end
183
+
184
+ # Builds the system-Gradle fallback command, gating wrapper flags on the system Gradle's own
185
+ # version (which may differ from the project's wrapper version).
186
+ sig do
187
+ params(
188
+ requirements: T::Array[Dependabot::DependencyRequirement],
189
+ original_document: T.nilable(Wrapper::PropertiesDocument),
190
+ cwd: String,
191
+ env: T::Hash[String, String]
192
+ ).returns(String)
193
+ end
194
+ def system_wrapper_command(requirements, original_document, cwd, env)
195
+ gradle_version = detect_system_gradle_version(cwd, env)
196
+ Shellwords.join(["gradle"] + build_command_parts(requirements, original_document, gradle_version))
197
+ end
198
+
153
199
  sig do
154
200
  params(
155
201
  requirements: T::Array[Dependabot::DependencyRequirement],
156
- network_timeout: T.nilable(String)
202
+ original_document: T.nilable(Wrapper::PropertiesDocument),
203
+ gradle_version: T.nilable(Dependabot::Gradle::Version)
157
204
  ).returns(T::Array[String])
158
205
  end
159
- def command_args(requirements, network_timeout)
160
- version = T.let(requirements[0]&.[](:requirement), String)
161
- checksum = T.let(requirements[1]&.[](:requirement), T.nilable(String)) if requirements.size > 1
162
- distribution_url = T.let(requirements[0]&.[](:source), T::Hash[Symbol, String])[:url]
163
- distribution_type = distribution_url&.match(/\b(bin|all)\b/)&.captures&.first
164
-
165
- args = %W(wrapper --gradle-version #{version})
166
-
167
- # Executing the wrapper task with `validateDistributionUrl=true`,
168
- # issues a HEAD request to ensure that the file exists and is reachable.
169
- # Example: HEAD https://services.gradle.org/distributions/gradle-9.3.0-bin.zip
170
- # Unfortunately, Dependabot's proxy does not seem to support something about this request
171
- # This causes the validation to fail and the wrapper task to error out
172
- # To work around this, we pass `--no-validate-url` to skip the url validation step,
173
- # Note: this temporarily sets `validateDistributionUrl=false` in `gradle-wrapper.properties`.
174
- # After the wrapper task completes, we restore the original value, since `--no-validate-url` would otherwise
175
- # persist the change in the properties file, which is not the behavior we want for users.
176
- # TODO: Investigate and fix the root cause of the proxy issue and remove this workaround
177
- # See https://github.com/dependabot/dependabot-core/issues/14036
178
- args += %w(--no-validate-url)
179
-
180
- # Gradle builds can be very complex, and our current Gradle parsing is limited.
181
- # To keep `./gradlew wrapper` running reliably, we generate a minimal build that omits the
182
- # project’s build scripts and customizations. As a result, any `tasks.wrapper {}` DSL configuration
183
- # defined in the original project is not applied.
184
- #
185
- # This approach, combined with https://github.com/gradle/gradle/issues/36172 where the wrapper task
186
- # relies on hardcoded defaults instead of reading from `gradle-wrapper.properties`, causes
187
- # `networkTimeout` customizations to be reset to the default value on every Dependabot pull request.
188
- #
189
- # This change mitigates the issue by reading the existing value and passing it explicitly to the
190
- # `wrapper` command, ensuring any custom `networkTimeout` setting is preserved.
191
- #
192
- # In future iterations, we may consider parsing the full Gradle build and extracting only the
193
- # wrapper-related customizations so the project-specific `tasks.wrapper {}` behavior is retained.
194
- # Alternatively, if Gradle addresses the upstream issue, we can revert to using the default minimal
195
- # build without needing explicit configuration.
196
- args += %W(--network-timeout #{network_timeout}) if network_timeout
197
-
198
- args += %W(--distribution-type #{distribution_type}) if distribution_type
199
- args += %W(--gradle-distribution-sha256-sum #{checksum}) if checksum
200
- args
206
+ def build_command_parts(requirements, original_document, gradle_version)
207
+ wrapper_args = Wrapper::CommandBuilder.new(
208
+ requirements: requirements,
209
+ original_properties: original_document,
210
+ gradle_version: gradle_version
211
+ ).build
212
+ %w(--no-daemon --stacktrace) + wrapper_args
213
+ end
214
+
215
+ sig { params(cwd: String, env: T::Hash[String, String]).returns(T.nilable(Dependabot::Gradle::Version)) }
216
+ def detect_system_gradle_version(cwd, env)
217
+ output = SharedHelpers.run_shell_command("gradle --version", cwd: cwd, env: env)
218
+ Wrapper::ExecutingVersionDetector.from_version_output(output)
219
+ rescue SharedHelpers::HelperSubprocessFailed => e
220
+ Dependabot.logger.warn("Unable to detect system Gradle version: #{e.message}")
221
+ nil
201
222
  end
202
223
 
203
224
  # Gradle builds can be complex, to maximize the chances of a successful we just keep related wrapper files
@@ -249,29 +270,27 @@ module Dependabot
249
270
  end
250
271
  end
251
272
 
252
- sig { params(properties_file: T.any(Pathname, String)).returns(T::Array[T.nilable(String)]) }
253
- def read_wrapper_options(properties_file)
254
- return [nil, nil] unless File.exist?(properties_file)
255
-
256
- properties_content = File.read(properties_file)
257
- validate_distribution = properties_content.match(/^validateDistributionUrl=(.*)$/)&.captures&.first
258
- network_timeout = properties_content.match(/^networkTimeout=(.*)$/)&.captures&.first
273
+ sig { params(path: T.any(Pathname, String)).returns(T.nilable(String)) }
274
+ def read_file(path)
275
+ return nil unless File.exist?(path)
259
276
 
260
- [validate_distribution, network_timeout]
277
+ File.read(path)
261
278
  end
262
279
 
263
- sig { params(properties_file: T.any(Pathname, String), value: T.nilable(String)).void }
264
- def override_validate_distribution_url_option(properties_file, value)
265
- return unless File.exist?(properties_file)
266
-
267
- properties_content = File.read(properties_file)
268
- updated_content = properties_content.gsub(
269
- /^validateDistributionUrl=(.*)\n/,
270
- value ? "validateDistributionUrl=#{value}\n" : ""
280
+ # Reconciles the wrapper-regenerated properties file back onto the user's original content,
281
+ # overwriting only the keys that legitimately change for a version bump. Preserves comments,
282
+ # ordering, custom keys and all other user-customized values.
283
+ sig { params(properties_file: T.any(Pathname, String), original_content: T.nilable(String)).void }
284
+ def reconcile_properties(properties_file, original_content)
285
+ regenerated_content = read_file(properties_file)
286
+ reconciled = Wrapper::PropertiesReconciler.reconcile(
287
+ original_content: original_content,
288
+ regenerated_content: regenerated_content
271
289
  )
272
- File.write(properties_file, updated_content)
290
+ File.write(properties_file, reconciled) if reconciled && reconciled != regenerated_content
273
291
  end
274
292
 
293
+ # rubocop:disable Metrics/PerceivedComplexity
275
294
  sig { returns(T::Array[String]) }
276
295
  def proxy_args
277
296
  http_proxy = ENV.fetch("HTTP_PROXY", nil)
@@ -15,6 +15,7 @@ require "dependabot/logger"
15
15
  require "dependabot/gradle/metadata_finder"
16
16
  require "dependabot/gradle/package/release_date_extractor"
17
17
  require "dependabot/gradle/package/version_release_date_fallback_fetcher"
18
+ require "dependabot/package/release_cooldown_options"
18
19
 
19
20
  module Dependabot
20
21
  module Gradle
@@ -55,14 +56,16 @@ module Dependabot
55
56
  dependency: Dependabot::Dependency,
56
57
  dependency_files: T::Array[Dependabot::DependencyFile],
57
58
  credentials: T::Array[Dependabot::Credential],
58
- forbidden_urls: T.nilable(T::Array[String])
59
+ forbidden_urls: T.nilable(T::Array[String]),
60
+ cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions)
59
61
  ).void
60
62
  end
61
- def initialize(dependency:, dependency_files:, credentials:, forbidden_urls:)
63
+ def initialize(dependency:, dependency_files:, credentials:, forbidden_urls:, cooldown_options: nil)
62
64
  @dependency = dependency
63
65
  @dependency_files = dependency_files
64
66
  @credentials = credentials
65
67
  @forbidden_urls = forbidden_urls
68
+ @cooldown_options = T.let(cooldown_options, T.nilable(Dependabot::Package::ReleaseCooldownOptions))
66
69
  @repositories = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]]))
67
70
  @google_version_details = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]]))
68
71
  @dependency_repository_details = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]]))
@@ -101,7 +104,7 @@ module Dependabot
101
104
  end.flatten.compact
102
105
 
103
106
  version_details = version_details.sort_by { |details| details.fetch(:version) }
104
- release_date_info = release_details
107
+ release_date_info = @cooldown_options ? release_details : {}
105
108
  version_details.each do |info|
106
109
  package_releases << {
107
110
  version: Gradle::Version.new(info[:version].to_s),
@@ -8,6 +8,7 @@
8
8
 
9
9
  require "sorbet-runtime"
10
10
 
11
+ require "dependabot/dependency_requirement"
11
12
  require "dependabot/requirements_updater/base"
12
13
  require "dependabot/gradle/distributions"
13
14
  require "dependabot/gradle/package/distributions_fetcher"
@@ -29,7 +30,7 @@ module Dependabot
29
30
 
30
31
  sig do
31
32
  params(
32
- requirements: T::Array[T::Hash[Symbol, T.untyped]],
33
+ requirements: T::Array[Dependabot::DependencyRequirement],
33
34
  latest_version: T.nilable(T.any(Version, String)),
34
35
  source_url: T.nilable(String),
35
36
  properties_to_update: T::Array[String]
@@ -51,7 +52,7 @@ module Dependabot
51
52
  @is_distribution = T.let(Distributions.distribution_requirements?(requirements), T::Boolean)
52
53
  end
53
54
 
54
- sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
55
+ sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
55
56
  def updated_requirements
56
57
  return requirements unless latest_version
57
58
  return updated_distribution_requirements if @is_distribution
@@ -67,13 +68,13 @@ module Dependabot
67
68
  next req if property_name && !properties_to_update.include?(property_name)
68
69
 
69
70
  new_req = update_requirement(req[:requirement])
70
- req.merge(requirement: new_req, source: updated_source)
71
+ Dependabot::DependencyRequirement.create(req.merge(requirement: new_req, source: updated_source))
71
72
  end
72
73
  end
73
74
 
74
75
  private
75
76
 
76
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
77
+ sig { returns(T::Array[Dependabot::DependencyRequirement]) }
77
78
  attr_reader :requirements
78
79
 
79
80
  sig { returns(T.nilable(Version)) }
@@ -118,7 +119,7 @@ module Dependabot
118
119
  end
119
120
  end
120
121
 
121
- sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
122
+ sig { returns(T::Array[Dependabot::DependencyRequirement]) }
122
123
  def updated_distribution_requirements
123
124
  distribution_url = T.let(nil, T.nilable(String))
124
125
 
@@ -132,15 +133,19 @@ module Dependabot
132
133
  version = update_exact_requirement(requirement)
133
134
  distribution_url = source[:url].gsub(requirement, version)
134
135
 
135
- req.merge(
136
- requirement: version,
137
- source: source.merge(url: distribution_url)
136
+ Dependabot::DependencyRequirement.create(
137
+ req.merge(
138
+ requirement: version,
139
+ source: source.merge(url: distribution_url)
140
+ )
138
141
  )
139
142
  when "distributionSha256Sum"
140
143
  checksum_url, checksum = Gradle::Package::DistributionsFetcher.resolve_checksum(T.must(distribution_url))
141
- req.merge(
142
- requirement: checksum,
143
- source: source.merge(url: checksum_url)
144
+ Dependabot::DependencyRequirement.create(
145
+ req.merge(
146
+ requirement: checksum,
147
+ source: source.merge(url: checksum_url)
148
+ )
144
149
  )
145
150
  else
146
151
  next req
@@ -124,7 +124,8 @@ module Dependabot
124
124
  dependency: dependency,
125
125
  dependency_files: dependency_files,
126
126
  credentials: credentials,
127
- forbidden_urls: forbidden_urls
127
+ forbidden_urls: forbidden_urls,
128
+ cooldown_options: cooldown_options
128
129
  ).fetch_available_versions
129
130
  end
130
131
 
@@ -169,7 +170,8 @@ module Dependabot
169
170
  dependency: dependency,
170
171
  dependency_files: dependency_files,
171
172
  credentials: credentials,
172
- forbidden_urls: []
173
+ forbidden_urls: [],
174
+ cooldown_options: cooldown_options
173
175
  ),
174
176
  T.nilable(Dependabot::Gradle::Package::PackageDetailsFetcher)
175
177
  )
@@ -68,14 +68,12 @@ module Dependabot
68
68
  declarations_using_a_property
69
69
  .map { |req| req.dig(:metadata, :property_name) }
70
70
 
71
- wrap_requirements(
72
- RequirementsUpdater.new(
73
- requirements: dependency.requirements,
74
- latest_version: preferred_resolvable_version&.to_s,
75
- source_url: preferred_version_details&.fetch(:source_url),
76
- properties_to_update: property_names
77
- ).updated_requirements
78
- )
71
+ RequirementsUpdater.new(
72
+ requirements: dependency.requirements,
73
+ latest_version: preferred_resolvable_version&.to_s,
74
+ source_url: preferred_version_details&.fetch(:source_url),
75
+ properties_to_update: property_names
76
+ ).updated_requirements
79
77
  end
80
78
 
81
79
  sig { override.returns(T::Boolean) }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-gradle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.382.0
4
+ version: 0.383.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.382.0
18
+ version: 0.383.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.382.0
25
+ version: 0.383.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: dependabot-maven
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.382.0
32
+ version: 0.383.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.382.0
39
+ version: 0.383.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: debug
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -267,6 +267,11 @@ files:
267
267
  - lib/dependabot/gradle/file_updater/dependency_set_updater.rb
268
268
  - lib/dependabot/gradle/file_updater/lockfile_updater.rb
269
269
  - lib/dependabot/gradle/file_updater/property_value_updater.rb
270
+ - lib/dependabot/gradle/file_updater/wrapper/command_builder.rb
271
+ - lib/dependabot/gradle/file_updater/wrapper/executing_version_detector.rb
272
+ - lib/dependabot/gradle/file_updater/wrapper/gradle_version_capabilities.rb
273
+ - lib/dependabot/gradle/file_updater/wrapper/properties_document.rb
274
+ - lib/dependabot/gradle/file_updater/wrapper/properties_reconciler.rb
270
275
  - lib/dependabot/gradle/file_updater/wrapper_updater.rb
271
276
  - lib/dependabot/gradle/language.rb
272
277
  - lib/dependabot/gradle/metadata_finder.rb
@@ -286,7 +291,7 @@ licenses:
286
291
  - MIT
287
292
  metadata:
288
293
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
289
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.382.0
294
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.383.0
290
295
  rdoc_options: []
291
296
  require_paths:
292
297
  - lib