dependabot-gradle 0.381.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: 47a95a22f6928d61384fbdcad6a0f380e947cde49bb5e2b441fc30e9f878ea30
4
- data.tar.gz: c2fbefa823a3cac3888e0a3976d0d17ae79a23501f124db73f344c73788ab42e
3
+ metadata.gz: d3cb4e728e6f95cd0ba68227af68fd31b0e30ff55a7c28090bfd7f9bc0a357bd
4
+ data.tar.gz: 268f0b46c9fce142acc4f4b615803481e6e9a72844aafcaee0b18becccd471ef
5
5
  SHA512:
6
- metadata.gz: 92d0bfb78cfaf20469a27f89908bf0e1552d84f04b9a88dfe417529518be7773ed60e44851697885fb92f728d21055d21b95653d754161c8df607e4b516fe0e5
7
- data.tar.gz: b046a8c623cf05a6375db35f3a6bbb983b4fb94ae764cddd925bf0a66a32df5af0546128bae318496dbbd3654027bd571f219e5d77fc08de9077cfd57860ee34
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
@@ -26,30 +26,34 @@ module Dependabot
26
26
  version = match.fetch("version")
27
27
 
28
28
  requirements = T.let(
29
- [{
30
- requirement: version,
31
- file: properties_file.name,
32
- source: {
33
- type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
34
- url: distribution_url,
35
- property: "distributionUrl"
36
- },
37
- groups: []
38
- }],
39
- T::Array[T::Hash[Symbol, T.untyped]]
29
+ [DependencyRequirement.create(
30
+ {
31
+ requirement: version,
32
+ file: properties_file.name,
33
+ source: {
34
+ type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
35
+ url: distribution_url,
36
+ property: "distributionUrl"
37
+ },
38
+ groups: []
39
+ }
40
+ )],
41
+ T::Array[Dependabot::DependencyRequirement]
40
42
  )
41
43
 
42
44
  if checksum
43
- requirements << {
44
- requirement: checksum,
45
- file: properties_file.name,
46
- source: {
47
- type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
48
- url: "#{distribution_url}.sha256",
49
- property: "distributionSha256Sum"
50
- },
51
- groups: []
52
- }
45
+ requirements << DependencyRequirement.create(
46
+ {
47
+ requirement: checksum,
48
+ file: properties_file.name,
49
+ source: {
50
+ type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE,
51
+ url: "#{distribution_url}.sha256",
52
+ property: "distributionSha256Sum"
53
+ },
54
+ groups: []
55
+ }
56
+ )
53
57
  end
54
58
 
55
59
  Dependency.new(
@@ -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
@@ -2,9 +2,12 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
+ require "base64"
5
6
  require "shellwords"
7
+ require "pathname"
6
8
 
7
9
  require "dependabot/gradle/distributions"
10
+ require "dependabot/gradle/version"
8
11
 
9
12
  module Dependabot
10
13
  module Gradle
@@ -13,6 +16,11 @@ module Dependabot
13
16
  extend T::Sig
14
17
  include Dependabot::Gradle::Distributions
15
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
+
16
24
  sig { params(dependency_files: T::Array[Dependabot::DependencyFile], dependency: Dependabot::Dependency).void }
17
25
  def initialize(dependency_files:, dependency:)
18
26
  @dependency_files = dependency_files
@@ -39,15 +47,12 @@ module Dependabot
39
47
 
40
48
  # rubocop:disable Metrics/AbcSize
41
49
  # rubocop:disable Metrics/MethodLength
42
- # rubocop:disable Metrics/PerceivedComplexity
43
50
  sig { params(build_file: Dependabot::DependencyFile).returns(T::Array[Dependabot::DependencyFile]) }
44
51
  def update_files(build_file)
45
52
  # We only run this updater if it's a distribution dependency
46
53
  return [] unless Distributions.distribution_requirements?(dependency.requirements)
47
54
 
48
- local_files = dependency_files.select do |file|
49
- file.directory == build_file.directory && target_file?(file)
50
- end
55
+ local_files = local_wrapper_files(build_file)
51
56
 
52
57
  # If we don't have any files in the build files don't generate one
53
58
  return [] unless local_files.any?
@@ -69,14 +74,11 @@ module Dependabot
69
74
  FileUtils.chmod("+x", "./gradlew") if has_local_script
70
75
 
71
76
  properties_file = File.join(cwd, "gradle/wrapper/gradle-wrapper.properties")
72
- 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)
73
79
  env = { "JAVA_OPTS" => proxy_args.join(" ") } # set proxy for gradle execution
74
80
 
75
- command_parts = %w(--no-daemon --stacktrace) + command_args(target_requirements, network_timeout)
76
-
77
- # There is no guarantee that the `gradlew` script is present on the project,
78
- # if it's not, we fall back to system Gradle
79
- command = Shellwords.join([has_local_script ? "./gradlew" : "gradle"] + command_parts)
81
+ command = local_wrapper_command(has_local_script, target_requirements, original_document, cwd, env)
80
82
 
81
83
  begin
82
84
  # first attempt: run the wrapper task via the local Gradle wrapper (if present)
@@ -84,18 +86,23 @@ module Dependabot
84
86
  # the `gradlew` script may be corrupted, so we try and fall back to system Gradle before giving up
85
87
  SharedHelpers.run_shell_command(command, cwd: cwd, env: env)
86
88
  rescue SharedHelpers::HelperSubprocessFailed => e
87
- 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
88
90
 
89
91
  Dependabot.logger.warn("Running #{command} failed, retrying first with system Gradle: #{e.message}")
90
92
 
91
- # second attempt: run the wrapper task via system gradle and then retry via local wrapper
92
- 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)
93
97
  SharedHelpers.run_shell_command(system_command, cwd: cwd, env: env) # run via system gradle
94
98
  SharedHelpers.run_shell_command(command, cwd: cwd, env: env) # retry via local wrapper
95
99
  end
96
100
 
97
- # Restore previous validateDistributionUrl option if it existed
98
- 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)
99
106
 
100
107
  update_files_content(temp_dir, local_files, updated_files)
101
108
  rescue SharedHelpers::HelperSubprocessFailed => e
@@ -107,7 +114,6 @@ module Dependabot
107
114
  end
108
115
  # rubocop:enable Metrics/AbcSize
109
116
  # rubocop:enable Metrics/MethodLength
110
- # rubocop:enable Metrics/PerceivedComplexity
111
117
 
112
118
  private
113
119
 
@@ -116,50 +122,103 @@ module Dependabot
116
122
  @target_files.any? { |r| "/#{file.name}".end_with?(r) }
117
123
  end
118
124
 
119
- # rubocop:disable Metrics/PerceivedComplexity
120
- sig { params(requirements: T::Array[T::Hash[Symbol, T.untyped]], network_timeout: T.nilable(String)).returns(T::Array[String]) }
121
- def command_args(requirements, network_timeout)
122
- version = T.let(requirements[0]&.[](:requirement), String)
123
- checksum = T.let(requirements[1]&.[](:requirement), T.nilable(String)) if requirements.size > 1
124
- distribution_url = T.let(requirements[0]&.[](:source), T::Hash[Symbol, String])[:url]
125
- distribution_type = distribution_url&.match(/\b(bin|all)\b/)&.captures&.first
126
-
127
- args = %W(wrapper --gradle-version #{version})
128
-
129
- # Executing the wrapper task with `validateDistributionUrl=true`,
130
- # issues a HEAD request to ensure that the file exists and is reachable.
131
- # Example: HEAD https://services.gradle.org/distributions/gradle-9.3.0-bin.zip
132
- # Unfortunately, Dependabot's proxy does not seem to support something about this request
133
- # This causes the validation to fail and the wrapper task to error out
134
- # To work around this, we pass `--no-validate-url` to skip the url validation step,
135
- # Note: this temporarily sets `validateDistributionUrl=false` in `gradle-wrapper.properties`.
136
- # After the wrapper task completes, we restore the original value, since `--no-validate-url` would otherwise
137
- # persist the change in the properties file, which is not the behavior we want for users.
138
- # TODO: Investigate and fix the root cause of the proxy issue and remove this workaround
139
- # See https://github.com/dependabot/dependabot-core/issues/14036
140
- args += %w(--no-validate-url)
141
-
142
- # Gradle builds can be very complex, and our current Gradle parsing is limited.
143
- # To keep `./gradlew wrapper` running reliably, we generate a minimal build that omits the
144
- # project’s build scripts and customizations. As a result, any `tasks.wrapper {}` DSL configuration
145
- # defined in the original project is not applied.
146
- #
147
- # This approach, combined with https://github.com/gradle/gradle/issues/36172 where the wrapper task
148
- # relies on hardcoded defaults instead of reading from `gradle-wrapper.properties`, causes
149
- # `networkTimeout` customizations to be reset to the default value on every Dependabot pull request.
150
- #
151
- # This change mitigates the issue by reading the existing value and passing it explicitly to the
152
- # `wrapper` command, ensuring any custom `networkTimeout` setting is preserved.
153
- #
154
- # In future iterations, we may consider parsing the full Gradle build and extracting only the
155
- # wrapper-related customizations so the project-specific `tasks.wrapper {}` behavior is retained.
156
- # Alternatively, if Gradle addresses the upstream issue, we can revert to using the default minimal
157
- # build without needing explicit configuration.
158
- args += %W(--network-timeout #{network_timeout}) if network_timeout
159
-
160
- args += %W(--distribution-type #{distribution_type}) if distribution_type
161
- args += %W(--gradle-distribution-sha256-sum #{checksum}) if checksum
162
- args
125
+ sig { params(build_file: Dependabot::DependencyFile).returns(T::Array[Dependabot::DependencyFile]) }
126
+ def local_wrapper_files(build_file)
127
+ wrapper_root = wrapper_root_for(build_file)
128
+
129
+ dependency_files.select do |file|
130
+ file.directory == build_file.directory && target_file_for_wrapper_root?(file, wrapper_root)
131
+ end
132
+ end
133
+
134
+ sig { params(file: Dependabot::DependencyFile, wrapper_root: String).returns(T::Boolean) }
135
+ def target_file_for_wrapper_root?(file, wrapper_root)
136
+ @target_files.any? do |target_file|
137
+ target_path = target_file.delete_prefix("/")
138
+ expected_path = wrapper_root.empty? ? target_path : File.join(wrapper_root, target_path)
139
+ file_path(file) == Pathname.new(expected_path).cleanpath.to_path
140
+ end
141
+ end
142
+
143
+ sig { params(build_file: Dependabot::DependencyFile).returns(String) }
144
+ def wrapper_root_for(build_file)
145
+ path = file_path(build_file)
146
+ root = if target_file?(build_file)
147
+ File.dirname(path, 3)
148
+ else
149
+ File.dirname(path)
150
+ end
151
+
152
+ root == "." ? "" : root
153
+ end
154
+
155
+ sig { params(file: Dependabot::DependencyFile).returns(String) }
156
+ def file_path(file)
157
+ Pathname.new(file.name).cleanpath.to_path
158
+ end
159
+
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
+
199
+ sig do
200
+ params(
201
+ requirements: T::Array[Dependabot::DependencyRequirement],
202
+ original_document: T.nilable(Wrapper::PropertiesDocument),
203
+ gradle_version: T.nilable(Dependabot::Gradle::Version)
204
+ ).returns(T::Array[String])
205
+ end
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
163
222
  end
164
223
 
165
224
  # Gradle builds can be complex, to maximize the chances of a successful we just keep related wrapper files
@@ -191,7 +250,11 @@ module Dependabot
191
250
  end
192
251
  def update_files_content(temp_dir, local_files, updated_files)
193
252
  local_files.each do |file|
194
- f_content = File.read(File.join(temp_dir, file.directory, file.name))
253
+ f_content = if file.binary?
254
+ File.binread(File.join(temp_dir, file.directory, file.name))
255
+ else
256
+ File.read(File.join(temp_dir, file.directory, file.name))
257
+ end
195
258
  tmp_file = file.dup
196
259
  tmp_file.content = tmp_file.binary? ? Base64.encode64(f_content) : f_content
197
260
  updated_files[T.must(updated_files.index(file))] = tmp_file
@@ -203,33 +266,31 @@ module Dependabot
203
266
  files_to_populate.each do |file|
204
267
  in_path_name = File.join(temp_dir, file.directory, file.name)
205
268
  FileUtils.mkdir_p(File.dirname(in_path_name))
206
- File.write(in_path_name, file.content)
269
+ File.binwrite(in_path_name, file.decoded_content)
207
270
  end
208
271
  end
209
272
 
210
- sig { params(properties_file: T.any(Pathname, String)).returns(T::Array[T.nilable(String)]) }
211
- def read_wrapper_options(properties_file)
212
- return [nil, nil] unless File.exist?(properties_file)
213
-
214
- properties_content = File.read(properties_file)
215
- validate_distribution = properties_content.match(/^validateDistributionUrl=(.*)$/)&.captures&.first
216
- 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)
217
276
 
218
- [validate_distribution, network_timeout]
277
+ File.read(path)
219
278
  end
220
279
 
221
- sig { params(properties_file: T.any(Pathname, String), value: T.nilable(String)).void }
222
- def override_validate_distribution_url_option(properties_file, value)
223
- return unless File.exist?(properties_file)
224
-
225
- properties_content = File.read(properties_file)
226
- updated_content = properties_content.gsub(
227
- /^validateDistributionUrl=(.*)\n/,
228
- 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
229
289
  )
230
- File.write(properties_file, updated_content)
290
+ File.write(properties_file, reconciled) if reconciled && reconciled != regenerated_content
231
291
  end
232
292
 
293
+ # rubocop:disable Metrics/PerceivedComplexity
233
294
  sig { returns(T::Array[String]) }
234
295
  def proxy_args
235
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
  )
@@ -62,7 +62,7 @@ module Dependabot
62
62
  nil
63
63
  end
64
64
 
65
- sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
65
+ sig { override.returns(T::Array[Dependabot::DependencyRequirement]) }
66
66
  def updated_requirements
67
67
  property_names =
68
68
  declarations_using_a_property
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.381.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.381.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.381.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.381.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.381.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.381.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