diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 20fc4c64f91..272831eaa12 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -14,7 +14,7 @@ concurrency: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SMOKE_TEST_BRANCH: ${{ vars.SMOKE_TEST_BRANCH || 'main' }} + SMOKE_TEST_BRANCH: maven-wrapper permissions: {} jobs: @@ -49,7 +49,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/${{ vars.SMOKE_TEST_REPO || 'dependabot/smoke-tests' }}/contents/tests?ref=${{ env.SMOKE_TEST_BRANCH }} + URL=https://api.github.com/repos/${{ vars.SMOKE_TEST_REPO || 'yeikel/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 @@ -97,7 +97,7 @@ jobs: - name: Download test if: steps.cache-smoke-test.outputs.cache-hit != 'true' run: | - gh api "repos/${{ vars.SMOKE_TEST_REPO || 'dependabot/smoke-tests' }}/contents/tests/${{ matrix.suite.name }}?ref=${{ env.SMOKE_TEST_BRANCH }}" -H "Accept: application/vnd.github.raw" > smoke.yaml + gh api "repos/${{ vars.SMOKE_TEST_REPO || 'yeikel/smoke-tests' }}/contents/tests/${{ matrix.suite.name }}?ref=maven-wrapper" -H "Accept: application/vnd.github.raw" > smoke.yaml - name: Cache Smoke Test if: steps.cache-smoke-test.outputs.cache-hit != 'true' @@ -117,7 +117,7 @@ jobs: CACHE=${CACHE%.yaml} CACHE=${CACHE%.yml} - gh run download --repo dependabot/smoke-tests --name cache-$CACHE --dir cache + gh run download --repo yeikel/smoke-tests --name cache-$CACHE --dir cache continue-on-error: true - name: Build ecosystem image diff --git a/maven/lib/dependabot/maven/distributions.rb b/maven/lib/dependabot/maven/distributions.rb new file mode 100644 index 00000000000..2e0002850e6 --- /dev/null +++ b/maven/lib/dependabot/maven/distributions.rb @@ -0,0 +1,29 @@ +# typed: strict +# frozen_string_literal: true + +module Dependabot + module Maven + module Distributions + extend T::Sig + + # Used to distinguish wrapper requirements (which live in maven-wrapper.properties) + # from regular POM requirements (which live in pom.xml) + DISTRIBUTION_DEPENDENCY_TYPE = "maven-distribution" + + # Maven and the maven-wrapper plugin release independently with separate cadences. + # Tracking them as distinct dependencies allows users to update each on their own + # schedule. Users who prefer batched updates can use grouped updates. + + MAVEN_DISTRIBUTION_PACKAGE = "org.apache.maven:apache-maven" + MAVEN_WRAPPER_PACKAGE = "org.apache.maven.wrapper:maven-wrapper" + + sig { params(requirements: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Boolean) } + def self.distribution_requirements?(requirements) + # Returns true if any requirement came from a maven-wrapper.properties + # file rather than a pom.xml. Used as the primary guard throughout the + # updater pipeline to short-circuit non-wrapper paths. + requirements.any? { |req| req.dig(:source, :type) == DISTRIBUTION_DEPENDENCY_TYPE } + end + end + end +end diff --git a/maven/lib/dependabot/maven/file_fetcher.rb b/maven/lib/dependabot/maven/file_fetcher.rb index a12bff5617b..5320595fd90 100644 --- a/maven/lib/dependabot/maven/file_fetcher.rb +++ b/maven/lib/dependabot/maven/file_fetcher.rb @@ -1,12 +1,15 @@ # typed: strict # frozen_string_literal: true +require "base64" require "nokogiri" require "sorbet-runtime" require "dependabot/file_fetchers" require "dependabot/file_fetchers/base" require "dependabot/file_filtering" +require "dependabot/experiments" +require "dependabot/maven/file_parser/wrapper_mojo" module Dependabot module Maven @@ -17,6 +20,14 @@ class FileFetcher < Dependabot::FileFetchers::Base MODULE_SELECTOR = "project > modules > module, " \ "profile > modules > module" + WRAPPER_PROPERTIES_RELATIVE = ".mvn/wrapper/maven-wrapper.properties" + WRAPPER_JAR_RELATIVE = ".mvn/wrapper/maven-wrapper.jar" + WRAPPER_DOWNLOADER_RELATIVE = ".mvn/wrapper/MavenWrapperDownloader.java" + + WRAPPER_UNIX_SCRIPTS = %w(mvnw mvnwDebug).freeze + WRAPPER_WINDOWS_SCRIPTS = %w(mvnw.cmd mvnwDebug.cmd).freeze + WRAPPER_ALL_SCRIPTS = T.let((WRAPPER_UNIX_SCRIPTS + WRAPPER_WINDOWS_SCRIPTS).freeze, T::Array[String]) + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) filenames.include?("pom.xml") @@ -31,10 +42,13 @@ def self.required_files_message def fetch_files fetched_files = [] fetched_files << pom - fetched_files += child_poms + poms = child_poms + fetched_files += poms fetched_files += relative_path_parents(fetched_files) fetched_files += targetfiles fetched_files << extensions if extensions + # Pass already-fetched poms so all_wrapper_files does not re-fetch them. + fetched_files += all_wrapper_files([T.must(pom)] + poms) # Filter excluded files from final collection filtered_files = fetched_files.uniq.reject do |file| @@ -46,6 +60,60 @@ def fetch_files private + sig { params(dir: String).returns(T::Array[DependencyFile]) } + def wrapper_files_for_dir(dir) + return [] unless Dependabot::Experiments.enabled?(:maven_wrapper_updater) + + # Strip leading "./" from root-level paths + properties_path = File.join(dir, WRAPPER_PROPERTIES_RELATIVE).delete_prefix("./") + properties = fetch_file_if_present(properties_path) + return [] unless properties + + files = T.let([properties], T::Array[DependencyFile]) + WRAPPER_ALL_SCRIPTS.each do |script| + script_path = dir == "." ? script : File.join(dir, script) + f = fetch_file_if_present(script_path) + files << f if f + end + + dist_type = FileParser::WrapperMojo.resolve_distribution_type(T.must(properties.content)) + files + fetch_wrapper_artifact_files(dir, dist_type) + rescue Dependabot::DependencyFileNotFound + [] + end + + sig { params(dir: String, dist_type: String).returns(T::Array[DependencyFile]) } + def fetch_wrapper_artifact_files(dir, dist_type) + case dist_type + when "bin", "script" + jar_path = File.join(dir, WRAPPER_JAR_RELATIVE).delete_prefix("./") + jar = fetch_file_if_present(jar_path) + return [] unless jar + + jar.content = Base64.encode64(T.must(jar.content)) if jar.content + jar.content_encoding = DependencyFile::ContentEncoding::BASE64 + [jar] + when "source" + dl_path = File.join(dir, WRAPPER_DOWNLOADER_RELATIVE).delete_prefix("./") + downloader = fetch_file_if_present(dl_path) + downloader ? [downloader] : [] + else + [] + end + end + + sig { params(poms: T::Array[DependencyFile]).returns(T::Array[DependencyFile]) } + def all_wrapper_files(poms) + seen_dirs = T.let(Set.new, T::Set[String]) + poms.filter_map do |pom_file| + dir = File.dirname(pom_file.name) + next if seen_dirs.include?(dir) + + seen_dirs << dir + wrapper_files_for_dir(dir) + end.flatten + end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def pom @pom ||= T.let(fetch_file_from_host("pom.xml"), T.nilable(Dependabot::DependencyFile)) diff --git a/maven/lib/dependabot/maven/file_parser.rb b/maven/lib/dependabot/maven/file_parser.rb index 0759e41e134..0649b07df54 100644 --- a/maven/lib/dependabot/maven/file_parser.rb +++ b/maven/lib/dependabot/maven/file_parser.rb @@ -24,6 +24,7 @@ class FileParser < Dependabot::FileParsers::Base require "dependabot/file_parsers/base/dependency_set" require_relative "file_parser/maven_dependency_parser" require_relative "file_parser/property_value_finder" + require_relative "file_parser/wrapper_mojo" # The following "dependencies" are candidates for updating: # - The project's parent @@ -93,6 +94,17 @@ def parse_standard_dependencies pomfiles.each { |pom| dependency_set += pomfile_dependencies(pom) } extensionfiles.each { |extension| dependency_set += extensionfile_dependencies(extension) } targetfiles.each { |target| dependency_set += targetfile_dependencies(target) } + + if Dependabot::Experiments.enabled?(:maven_wrapper_updater) + wrapper_properties_files.each do |properties_file| + dir = File.dirname(properties_file.name).sub(%r{/\.mvn/wrapper$}, "") + scripts = wrapper_script_files_for(dir) + FileParser::WrapperMojo.resolve_dependencies(properties_file, script_files: scripts).each do |dep| + dependency_set << dep + end + end + end + dependency_set.dependencies end @@ -470,6 +482,22 @@ def targetfiles ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } + def wrapper_properties_files + @wrapper_properties_files ||= T.let( + dependency_files.select { |f| f.name.end_with?("maven-wrapper.properties") }, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) + end + + sig { params(dir: String).returns(T::Array[Dependabot::DependencyFile]) } + def wrapper_script_files_for(dir) + script_names = %w(mvnw mvnw.cmd mvnwDebug mvnwDebug.cmd).map do |s| + dir == "." ? s : "#{dir}/#{s}" + end + dependency_files.select { |f| script_names.include?(f.name) } + end + sig { returns(T::Array[String]) } def internal_dependency_names @internal_dependency_names ||= T.let( diff --git a/maven/lib/dependabot/maven/file_parser/wrapper_mojo.rb b/maven/lib/dependabot/maven/file_parser/wrapper_mojo.rb new file mode 100644 index 00000000000..fb80553ae1d --- /dev/null +++ b/maven/lib/dependabot/maven/file_parser/wrapper_mojo.rb @@ -0,0 +1,370 @@ +# typed: strong +# frozen_string_literal: true + +# Parses maven-wrapper.properties and emits Dependency objects for the two +# tracked Maven coordinates: org.apache.maven:apache-maven (the maven distribution) +# and org.apache.maven.wrapper:maven-wrapper (the wrapper plugin). +require "dependabot/maven/file_parser" +require "dependabot/maven/distributions" + +module Dependabot + module Maven + class FileParser + # Model of the Maven Wrapper plugin's WrapperMojo goal. Mirrors the + # properties recognized by the upstream plugin: + # https://github.com/apache/maven-wrapper/blob/master/maven-wrapper-plugin/src/main/java/org/apache/maven/plugins/wrapper/WrapperMojo.java + class WrapperMojo + extend T::Sig + + class WrapperProperties < T::Struct + # Resolved distributionUrl value (the raw value from the properties file). + # This value is mandatory + const :distribution_url, String + # The Maven Version extracted from the distributionUrl, e.g. "3.9.9" + const :distribution_version, String + + # Value of distributionSha256Sum, or nil when the property is absent. + # Checksum verification is not mandatory + # Tracked as a second requirement on the Apache Maven dependency so the + # checksum is updated atomically with the version. + const :distribution_sha256_sum, T.nilable(String) + + # Value of wrapperSha256Sum, or nil when the property is absent. + # Used to verify the wrapper JAR download + const :wrapper_sha256_sum, T.nilable(String) + + # Version of the Maven Wrapper plugin, e.g. "3.3.4". + # Sourced from the first strategy that succeeds: + # - wrapperVersion property (>=3.3.1), + # - version segment of wrapperUrl JAR filename (<3.3.0), or + # - comment in the mvnw script body (3.3.0 only, see MWRAPPER-120 and MWRAPPER-134). + # This field is mandatory and raises if none of the sources yield a version. + const :wrapper_version, String + + # The full JAR URL from the wrapperUrl property + # (e.g. "https://.../maven-wrapper-3.3.3.jar"), used to compute the + # new download URL for wrapperSha256Sum recomputation. + # Present in both old-format (wrapperUrl only) and new-format (wrapperVersion + wrapperUrl) files. + # nil only when the version was read from the mvnw script body (very old wrappers). + const :wrapper_url, T.nilable(String) + + # Value of distributionType, controlling which binary artifacts are committed + # alongside the wrapper scripts: + # - "bin" (JAR present), + # - "script" (JAR present), + # - "only-script" (no JAR), + # - "source" (MavenWrapperDownloader.java present). + # Defaults to "bin" when the property is absent, matching pre-3.3.0 behavior. + const :distribution_type, String + end + + # Extracts the version from a distributionUrl value (the resolved URL, + # not the raw properties line). Matches the version from the directory + # path segment (e.g. `.../apache-maven/3.9.9/apache-maven-3.9.9-bin.zip`) + # to avoid capturing classifiers like `-bin` or `-src` as part of the version. + DIST_URL_VERSION_REGEX = %r{/apache-maven/(?[^/]+)/apache-maven-}x + + sig do + params( + properties_file: DependencyFile, + script_files: T::Array[DependencyFile] + ).returns(T::Array[Dependency]) + end + def self.resolve_dependencies(properties_file, script_files: []) + content = properties_file.content + return [] unless content + + distribution_url = get_property_value(content, "distributionUrl") + if distribution_url&.include?("mvnd") + Dependabot.logger.warn("Maven daemon (mvnd) distribution is not supported, skipping wrapper update") + return [] + end + + props = load_properties(content, script_files: script_files) + + if props.wrapper_url&.include?("takari") + Dependabot.logger.warn("The Takari distribution is not supported, skipping wrapper update") + return [] + end + + file_name = properties_file.name + has_debug_scripts = debug_scripts?(script_files) + deps = [build_distribution_dependency(file_name, props, props.distribution_version, has_debug_scripts)] + deps << build_wrapper_dependency( + file_name, props, props.distribution_version, props.wrapper_version, has_debug_scripts + ) + deps.compact + end + + sig do + params( + file_name: String, + props: WrapperProperties, + dist_version: String, + has_debug_scripts: T::Boolean + ).returns(Dependency) + end + def self.build_distribution_dependency(file_name, props, dist_version, has_debug_scripts) + Dependency.new( + name: Distributions::MAVEN_DISTRIBUTION_PACKAGE, + version: dist_version, + requirements: build_distribution_requirements(file_name, props, dist_version, has_debug_scripts), + package_manager: "maven" + ) + end + + sig do + params( + file_name: String, + props: WrapperProperties, + dist_version: String, + has_debug_scripts: T::Boolean + ).returns(T::Array[T::Hash[Symbol, T.untyped]]) + end + def self.build_distribution_requirements(file_name, props, dist_version, has_debug_scripts) + metadata = T.let( + { + packaging_type: "pom", + wrapper_version: props.wrapper_version, + distribution_type: props.distribution_type, + distribution_version: dist_version, + include_debug_script: has_debug_scripts + }, + T::Hash[Symbol, T.untyped] + ) + metadata[:distribution_sha256_sum] = props.distribution_sha256_sum if props.distribution_sha256_sum + + main_req = T.let( + { + requirement: dist_version, + file: file_name, + source: { + type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE, + url: props.distribution_url, + property: "distributionUrl" + }, + groups: [], + # The Apache Maven distribution is not a JAR, but a POM is available + # We can use this POM to query for new versions using the existing update checker for maven + metadata: metadata + }, + T::Hash[Symbol, T.untyped] + ) + + requirements = T.let([main_req], T::Array[T::Hash[Symbol, T.untyped]]) + requirements.concat(build_wrapper_url_requirement(file_name, props)) + requirements + end + + sig do + params( + file_name: String, + props: WrapperProperties + ).returns(T::Array[T::Hash[Symbol, T.untyped]]) + end + def self.build_wrapper_url_requirement(file_name, props) + return [] unless props.wrapper_url + + Dependabot.logger.debug "wrapperUrl is present #{props.wrapper_url} version=#{props.wrapper_version}" + metadata = T.let({}, T::Hash[Symbol, T.untyped]) + metadata[:wrapper_sha256_sum] = props.wrapper_sha256_sum if props.wrapper_sha256_sum + + req = T.let( + { + requirement: props.wrapper_version, + file: file_name, + source: { + type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE, + property: "wrapperUrl", + url: props.wrapper_url + }, + groups: [] + }, + T::Hash[Symbol, T.untyped] + ) + req[:metadata] = metadata if metadata.any? + [req] + end + + sig do + params( + file_name: String, + props: WrapperProperties, + dist_version: String, + wrapper_version: String, + has_debug_scripts: T::Boolean + ).returns(Dependency) + end + def self.build_wrapper_dependency(file_name, props, dist_version, wrapper_version, has_debug_scripts) + metadata = T.let( + { + packaging_type: "pom", + distribution_version: dist_version, + wrapper_version: wrapper_version, + distribution_type: props.distribution_type, + include_debug_script: has_debug_scripts + }, + T::Hash[Symbol, T.untyped] + ) + Dependency.new( + name: Distributions::MAVEN_WRAPPER_PACKAGE, + version: props.wrapper_version, + requirements: [ + { + requirement: props.wrapper_version, + file: file_name, + source: { + type: Distributions::DISTRIBUTION_DEPENDENCY_TYPE, + property: "wrapperVersion" + }, + groups: [], + metadata: metadata + } + ], + package_manager: "maven" + ) + end + + sig { params(content: String).returns(String) } + def self.resolve_distribution_type(content) + get_property_value(content, "distributionType") || "bin" + end + + sig { params(script_files: T::Array[DependencyFile]).returns(T::Boolean) } + def self.debug_scripts?(script_files) + debug_scripts = %w(mvnwDebug mvnwDebug.cmd) + script_files.any? { |f| debug_scripts.any? { |s| f.name.end_with?(s) } } + end + + sig { params(content: String, script_files: T::Array[DependencyFile]).returns(WrapperProperties) } + def self.load_properties(content, script_files: []) + distribution_url = get_property_value!(content, "distributionUrl") + distribution_version = extract_distribution_version(distribution_url) + distribution_sha256_sum = get_property_value(content, "distributionSha256Sum") + wrapper_url = get_property_value(content, "wrapperUrl") + wrapper_sha256_sum = get_property_value(content, "wrapperSha256Sum") + distribution_type = resolve_distribution_type(content) + wrapper_version = resolve_wrapper_version(content, wrapper_url, script_files) + + WrapperProperties.new( + distribution_url: distribution_url, + distribution_version: distribution_version, + distribution_sha256_sum: distribution_sha256_sum, + wrapper_sha256_sum: wrapper_sha256_sum, + wrapper_version: wrapper_version, + wrapper_url: wrapper_url, + distribution_type: distribution_type + ) + end + + sig { params(content: String).returns(String) } + def self.extract_distribution_version(content) + match = content.match(DIST_URL_VERSION_REGEX) + raise "Could not extract Maven version from content" unless match && match[:version] + + T.must(match[:version]) + end + + sig { params(content: String, target_key: String).returns(T.nilable(String)) } + def self.get_property_value(content, target_key) + # 1. Handle Java line continuations (the backslash edge case) + # We join lines ending in \ before splitting into an array + normalized_content = content.gsub(/\\\n\s*/, "") + + # 2. Escape the key for Regex safety + escaped_key = Regexp.escape(target_key) + + # 3. Define the pattern: + # Start of line -> Key -> optional space -> delimiter (= or :) -> value + pattern = /^\s*#{escaped_key}\s*[=:]\s*(.*)$/ + + normalized_content.lines.each do |line| + next if line.start_with?("#", "!") # Skip Java comments + + if (match = line.match(pattern)) + return T.must(match[1]).strip + end + end + nil + end + + sig { params(content: String, target_key: String).returns(String) } + def self.get_property_value!(content, target_key) + value = get_property_value(content, target_key) + + raise "Missing mandatory property: #{target_key}" if value.nil? + + value + end + + private_class_method :build_distribution_dependency + private_class_method :build_wrapper_dependency + private_class_method :build_distribution_requirements + private_class_method :build_wrapper_url_requirement + + # Matches the human-readable banner embedded in mvnw / mvnw.cmd, e.g.: + # "Apache Maven Wrapper startup script, version 3.3.2" + # "Apache Maven Wrapper startup batch script, version 3.3.2" + SCRIPT_VERSION_REGEX = / + Apache \s Maven \s Wrapper \s startup \s (?:batch \s)? + script, \s version \s + (?\d+\.\d+(?:\.\d+)?) + /x + + sig do + params( + content: String, + wrapper_url: T.nilable(String), + script_files: T::Array[DependencyFile] + ).returns(String) + end + def self.resolve_wrapper_version(content, wrapper_url, script_files) + version = get_property_value(content, "wrapperVersion") + return version if version + + version = parse_version_from_wrapper_url(wrapper_url) if wrapper_url + return version if version + + if script_files.any? + Dependabot.logger.warn "Maven Wrapper with no wrapperVersion or wrapperUrl in properties file" + version = load_wrapper_version_from_scripts(script_files) + end + + if version.nil? + raise "Could not determine Maven Wrapper version from wrapperVersion, wrapperUrl, or script files" + end + + version + end + + sig { params(url: String).returns(T.nilable(String)) } + def self.parse_version_from_wrapper_url(url) + match = url.match(/-(?\d+\.\d+(?:\.\d+)?(?:-\w+)*)(?:-bin)?\.jar/) + match&.[](:version) + end + + # Extracts the Maven Wrapper version declared in the mvnw / mvnw.cmd + # shell scripts. Unix scripts (mvnw) are checked before Windows scripts (mvnw.cmd) + # if neither contains the banner, nil is returned and the caller falls back to other sources. + sig { params(script_files: T::Array[DependencyFile]).returns(T.nilable(String)) } + def self.load_wrapper_version_from_scripts(script_files) + # Preferred order: Unix script first, Windows script as fallback. + windows_scripts, unix_scripts = script_files.partition { |f| f.name.end_with?(".cmd") } + unix_scripts.chain(windows_scripts).each do |file| + next unless file.content + + T.must(file.content).each_line do |line| + m = line.match(SCRIPT_VERSION_REGEX) + return m[:version] if m + end + end + nil + end + + private_class_method :resolve_wrapper_version + private_class_method :parse_version_from_wrapper_url + private_class_method :load_wrapper_version_from_scripts + end + end + end +end diff --git a/maven/lib/dependabot/maven/file_updater.rb b/maven/lib/dependabot/maven/file_updater.rb index 119f2f469a5..f845bc6815e 100644 --- a/maven/lib/dependabot/maven/file_updater.rb +++ b/maven/lib/dependabot/maven/file_updater.rb @@ -6,6 +6,8 @@ require "sorbet-runtime" require "dependabot/file_updaters" require "dependabot/file_updaters/base" +require "dependabot/experiments" +require "dependabot/maven/distributions" module Dependabot module Maven @@ -14,32 +16,58 @@ class FileUpdater < Dependabot::FileUpdaters::Base require_relative "file_updater/declaration_finder" require_relative "file_updater/property_value_updater" + require_relative "file_updater/wrapper_updater" sig { override.returns(T::Array[Dependabot::DependencyFile]) } def updated_dependency_files - updated_files = T.let(dependency_files.dup, T::Array[Dependabot::DependencyFile]) + all_updated = collect_wrapper_updates + collect_pom_updates + raise "No files changed!" if all_updated.none? + + all_updated + end + + private + + sig { returns(T::Array[Dependabot::DependencyFile]) } + def collect_wrapper_updates + return [] unless Dependabot::Experiments.enabled?(:maven_wrapper_updater) + + buildfile = dependency_files.find { |f| f.name == "pom.xml" } + return [] unless buildfile + + updated = T.let([], T::Array[Dependabot::DependencyFile]) + dependencies.each do |dependency| + next unless Distributions.distribution_requirements?(dependency.requirements) + wu = WrapperUpdater.new( + dependency_files: dependency_files, + dependency: dependency, + credentials: credentials + ) + updated += wu.update_files(buildfile) + end + updated + end + + sig { returns(T::Array[Dependabot::DependencyFile]) } + def collect_pom_updates # Loop through each of the changed requirements, applying changes to # all pom and extensions files for that change. Note that the logic # is different here to other package managers because Maven has property # inheritance across files + updated_files = T.let(dependency_files.dup, T::Array[Dependabot::DependencyFile]) dependencies.each do |dependency| + next if Distributions.distribution_requirements?(dependency.requirements) + updated_files = update_files_for_dependency( original_files: updated_files, dependency: dependency ) end - - updated_files.select! { |f| f.name.end_with?(".xml") } - updated_files.reject! { |f| dependency_files.include?(f) } - - raise "No files changed!" if updated_files.none? - - updated_files + updated_files.select { |f| f.name.end_with?(".xml") } + .reject { |f| dependency_files.include?(f) } end - private - sig { override.void } def check_required_files raise "No pom.xml!" unless get_original_file("pom.xml") diff --git a/maven/lib/dependabot/maven/file_updater/wrapper_updater.rb b/maven/lib/dependabot/maven/file_updater/wrapper_updater.rb new file mode 100644 index 00000000000..25a0474eab6 --- /dev/null +++ b/maven/lib/dependabot/maven/file_updater/wrapper_updater.rb @@ -0,0 +1,433 @@ +# typed: strict +# frozen_string_literal: true + +require "base64" +require "digest" +require "fileutils" +require "sorbet-runtime" +require "dependabot/errors" +require "dependabot/registry_client" +require "dependabot/shared_helpers" +require "dependabot/dependency_file" +require "dependabot/maven/distributions" +require "dependabot/maven/file_parser/wrapper_mojo" +require "dependabot/maven/file_updater" +require "dependabot/maven/native_helpers" + +module Dependabot + module Maven + class FileUpdater + class WrapperUpdater + extend T::Sig + + WRAPPER_PROPERTIES_RELATIVE = ".mvn/wrapper/maven-wrapper.properties" + JAR_RELATIVE = ".mvn/wrapper/maven-wrapper.jar" + DOWNLOADER_RELATIVE = ".mvn/wrapper/MavenWrapperDownloader.java" + + # Named constants for all wrapper scripts, split by platform. + # + # Every Unix shell script must have + # Mode::EXECUTABLE set after the update + # Windows executables can skip that as it carries no meaning + UNIX_SCRIPTS = %w(mvnw mvnwDebug).freeze + WINDOWS_SCRIPTS = %w(mvnw.cmd mvnwDebug.cmd).freeze + ALL_SCRIPTS = T.let((UNIX_SCRIPTS + WINDOWS_SCRIPTS).freeze, T::Array[String]) + + sig do + params( + dependency_files: T::Array[DependencyFile], + dependency: Dependency, + credentials: T::Array[Dependabot::Credential] + ).void + end + def initialize(dependency_files:, dependency:, credentials:) + @dependency_files = dependency_files + @dependency = dependency + @credentials = credentials + end + + # Entry point. Updates the wrapper properties file in-place, then + # regenerates shell scripts via the native wrapper:wrapper goal. + # Returns an empty array for non-wrapper dependencies. + sig { params(buildfile: DependencyFile).returns(T::Array[DependencyFile]) } + def update_files(buildfile) + # Return immediately for any non-wrapper dependency. + return [] unless Distributions.distribution_requirements?(dependency.requirements) + return [] unless wrapper_properties_file + + SharedHelpers.in_a_temporary_directory(project_root(buildfile)) do + write_dependency_files + + # Drop both SHA-256 checksum properties before invoking the native command. + # If they remain, Maven verifies the old checksum against the new version and fails. + # The checksums cannot be passed in during generation because the wrapper does not support it. + # As a workaround, we recompute and re-add them once the generation completes + strip_checksum_properties + + distribution_type = distribution_type_from_requirements + # Run the native wrapper command to regenerate the shell scripts + run_wrapper_command(distribution_type) + + # Maven Central only publishes SHA-512 checksums. + # But the wrapper only supports SHA-256 validation + # We need compute the SHA-256 digest directly from the artifact + # and write it into the regenerated properties file. + update_checksum_properties + + collect_updated_files(distribution_type) + end + end + + private + + sig { returns(Dependency) } + attr_reader :dependency + + # Removes distributionSha256Sum and wrapperSha256Sum from the on-disk + # properties file so that the wrapper:wrapper command can download the + # new artifact without Maven aborting on a stale checksum mismatch. + sig { void } + def strip_checksum_properties + return unless File.exist?(WRAPPER_PROPERTIES_RELATIVE) + + content = File.read(WRAPPER_PROPERTIES_RELATIVE) + stripped = content.lines.reject { |l| l.match?(/\A(?:distribution|wrapper)Sha256Sum\s*=/) }.join + File.write(WRAPPER_PROPERTIES_RELATIVE, stripped) + end + + # Computes a fresh digest from the updated artifact + # This is needed because Maven Central only publishes SHA-512 checksums, + # while the wrapper only supports validations for the SHA-256 + # + # Raises if an expected artifact cannot be found in the cache, because + # a stale or missing checksum would silently weaken integrity checking. + sig { returns(T.nilable(Integer)) } + def update_checksum_properties + dist_req = dependency.requirements.find { |r| r[:source][:property] == "distributionUrl" } + wrapper_req = dependency.requirements.find { |r| r[:source][:property] == "wrapperUrl" } + + dist_checksum = dist_req&.dig(:metadata, :distribution_sha256_sum) + wrapper_checksum = wrapper_req&.dig(:metadata, :wrapper_sha256_sum) + + Dependabot.logger.debug "updating checksum properties " \ + "dist=#{!dist_checksum.nil?} wrap=#{!wrapper_checksum.nil?}" + return unless dist_checksum || wrapper_checksum + return unless File.exist?(WRAPPER_PROPERTIES_RELATIVE) + + content = File.read(WRAPPER_PROPERTIES_RELATIVE) + + if dist_checksum + dist_url = dist_req.dig(:source, :url) + content = set_checksum_from_url( + content, + "distributionSha256Sum", + T.cast(dist_url, String) + ) + end + if wrapper_checksum + wrapper_url = wrapper_req.dig(:source, :url) + content = set_checksum_from_url( + content, + "wrapperSha256Sum", + T.cast(wrapper_url, String) + ) + end + File.write(WRAPPER_PROPERTIES_RELATIVE, content) + end + + sig { params(content: String, property_name: String, url: String).returns(String) } + def set_checksum_from_url(content, property_name, url) + sha256 = calculate_sha256_from_url(url) + set_checksum_property(content, property_name, sha256) + end + + sig { params(url: String).returns(String) } + def calculate_sha256_from_url(url) + @sha256_cache = T.let(@sha256_cache, T.nilable(T::Hash[String, String])) + @sha256_cache ||= {} + if @sha256_cache.key?(url) + Dependabot.logger.debug "SHA-256 cache hit for #{url}" + return T.must(@sha256_cache[url]) + end + + Dependabot.logger.info "Downloading Maven distribution: #{url} to calculate the sha256 checksum" + response = Dependabot::RegistryClient.get(url: url, headers: auth_headers_for_url(url)) + raise_on_auth_failure(url, response) + + raise "Failed to download #{url}: HTTP #{response.status}" unless response.status == 200 + + hash = Digest::SHA256.hexdigest(response.body) + Dependabot.logger.debug "Computed SHA-256: #{hash}" + @sha256_cache[url] = hash + rescue Dependabot::PrivateSourceAuthenticationFailure + raise + rescue StandardError => e + Dependabot.logger.error "Checksum computation failed with an unexpected error: #{e.message}" + raise + end + + sig { params(url: String, response: T.untyped).void } + def raise_on_auth_failure(url, response) + return unless response.status == 401 + + repository_url = url.match(%r{^(https?://[^/]+(?:/[^/]+)*)/org/})&.captures&.first || + URI.parse(url).host.to_s + raise Dependabot::PrivateSourceAuthenticationFailure, repository_url + end + + sig { params(url: String).returns(T::Hash[String, String]) } + def auth_headers_for_url(url) + Dependabot.logger.debug "Building auth headers for: #{url}" + + cred = maven_registry_credential(url) + unless cred + Dependabot.logger.debug "No matching credential found for #{url}, using no auth" + return {} + end + + username = cred["username"] + password = cred["password"] + unless username + Dependabot.logger.debug "Credential for #{url} has no username, using no auth" + return {} + end + + Dependabot.logger.debug "Using Basic auth for #{url} (username: #{username})" + { "Authorization" => "Basic #{Base64.strict_encode64("#{username}:#{password}")}" } + end + + sig { params(content: String, key: String, value: String).returns(String) } + def set_checksum_property(content, key, value) + Dependabot.logger.debug "Appending #{key}" + "#{content.rstrip}\n#{key}=#{value}\n" + end + + sig { params(distribution_type: String).void } + def run_wrapper_command(distribution_type) + distribution_version = distribution_version_from_requirements + wrapper_version = maven_wrapper_version_from_requirements + extra_args = build_extra_args_from_requirements + NativeHelpers.run_mvnw_wrapper( + version: distribution_version, + wrapper_plugin_version: wrapper_version, + env: build_env, + distribution_type: distribution_type, + extra_args: extra_args + ) + end + + sig { returns(String) } + def maven_wrapper_version_from_requirements + wrapper_version = dependency.requirements + .find { |r| r.dig(:metadata, :wrapper_version) } + &.dig(:metadata, :wrapper_version) + raise "Could not determine Maven Wrapper version from dependency requirements" unless wrapper_version + + T.cast(wrapper_version, String) + end + + sig { returns(String) } + def distribution_version_from_requirements + distribution_version = dependency.requirements + .find { |r| r.dig(:metadata, :distribution_version) } + &.dig(:metadata, :distribution_version) + raise "Could not determine distribution version from dependency requirements" unless distribution_version + + T.cast(distribution_version, String) + end + + sig { returns(String) } + def distribution_type_from_requirements + distribution_type = dependency.requirements + .find { |r| r.dig(:metadata, :distribution_type) } + &.dig(:metadata, :distribution_type) + raise "Could not determine distribution type from dependency requirements" unless distribution_type + + T.cast(distribution_type, String) + end + + sig { returns(T::Array[String]) } + def build_extra_args_from_requirements + args = T.let([], T::Array[String]) + include_debug = dependency.requirements + .find { |r| r.dig(:metadata, :include_debug_script) } + &.dig(:metadata, :include_debug_script) + args << "-DincludeDebugScript=true" if include_debug + args + end + + # Builds the environment hash passed to the native wrapper command, + # including proxy, registry URL, and credentials. + sig { returns(T::Hash[String, String]) } + def build_env + env = T.let({}, T::Hash[String, String]) + if (proxy = ENV.fetch("HTTPS_PROXY", nil)) + proxy_url = URI.parse(proxy) + Dependabot.logger.debug "Using proxy host: #{proxy_url.host}" + env["PROXY_HOST"] = proxy_url.host.to_s + end + + registry_base, cred = resolve_registry_base_and_credential + env.merge!(build_registry_env(registry_base, cred)) + env.merge!(build_credential_env(cred)) + + if Dependabot.logger.debug? + env["MVNW_VERBOSE"] = "true" + safe_env = env.except("MVNW_PASSWORD") + safe_env["MVNW_PASSWORD"] = "[REDACTED]" if env.key?("MVNW_PASSWORD") + Dependabot.logger.debug "build_env result: #{safe_env}" + end + + env + end + + sig do + returns([T.nilable(String), T.nilable(Dependabot::Credential)]) + end + def resolve_registry_base_and_credential + dist_req = dependency.requirements.find { |r| r[:source][:property] == "distributionUrl" } + dist_url = dist_req&.dig(:source, :url) + Dependabot.logger.debug "Distribution URL from requirements: #{dist_url}" + + registry_base_regex = %r{^(https?://[^/]+(?:/[^/]+)*)/org/apache/maven/apache-maven/} + registry_base = dist_url&.match(registry_base_regex)&.captures&.first + Dependabot.logger.debug "Extracted registry base: #{registry_base || '(none)'}" + + cred = maven_registry_credential(registry_base) + Dependabot.logger.debug "Matched credential: #{cred ? "url=#{cred.fetch('url', '(none)')}" : '(none)'}" + + [registry_base, cred] + end + + sig do + params(registry_base: T.nilable(String), cred: T.nilable(Dependabot::Credential)) + .returns(T::Hash[String, String]) + end + def build_registry_env(registry_base, cred) + if registry_base + { "MVNW_REPOURL" => registry_base } + elsif cred&.fetch("replaces_base", false) + { "MVNW_REPOURL" => cred.fetch("url").chomp("/") } + else + {} + end + end + + sig { params(cred: T.nilable(Dependabot::Credential)).returns(T::Hash[String, String]) } + def build_credential_env(cred) + return {} unless cred + + env = T.let({}, T::Hash[String, String]) + env["MVNW_USERNAME"] = T.must(cred["username"]) if cred["username"] + env["MVNW_PASSWORD"] = T.must(cred["password"]) if cred["password"] + env + end + + sig { params(registry_base: T.nilable(String)).returns(T.nilable(Dependabot::Credential)) } + def maven_registry_credential(registry_base) + maven_creds = @credentials.select { |c| c["type"] == "maven_repository" } + + if registry_base + url_matches = maven_creds.select do |c| + cred_url = c.fetch("url", "").chomp("/") + !cred_url.empty? && registry_base.start_with?(cred_url) + end + return url_matches.max_by { |c| c.fetch("url", "").length } if url_matches.any? + end + + maven_creds.find { |c| c.fetch("replaces_base", false) } + end + + sig { params(buildfile: DependencyFile).returns(String) } + def project_root(buildfile) + File.dirname(buildfile.path) + end + + # Assembles all updated files: properties, scripts, and the type-specific artifact. + sig { params(dist_type: String).returns(T::Array[DependencyFile]) } + def collect_updated_files(dist_type) + collect_properties_file + collect_script_files + collect_artifact_file(dist_type) + end + + # Returns a DependencyFile for the updated maven-wrapper.properties, or [] if absent. + sig { returns(T::Array[DependencyFile]) } + def collect_properties_file + return [] unless File.exist?(WRAPPER_PROPERTIES_RELATIVE) + + [DependencyFile.new( + name: WRAPPER_PROPERTIES_RELATIVE, + content: File.read(WRAPPER_PROPERTIES_RELATIVE), + directory: wrapper_properties_file&.directory || "/" + )] + end + + # Returns DependencyFiles for all wrapper scripts that exist on disk, + # marking Unix scripts as executable. + sig { returns(T::Array[DependencyFile]) } + def collect_script_files + ALL_SCRIPTS.filter_map do |script| + next unless File.exist?(script) + + file = DependencyFile.new( + name: script, + content: File.read(script), + directory: wrapper_properties_file&.directory || "/" + ) + file.mode = DependencyFile::Mode::EXECUTABLE if UNIX_SCRIPTS.include?(script) + file + end + end + + # Returns the type-specific artifact: the wrapper JAR for "bin"/"script", + # MavenWrapperDownloader.java for "source", or [] for "only-script". + sig { params(dist_type: String).returns(T::Array[DependencyFile]) } + def collect_artifact_file(dist_type) + case dist_type + when "bin", "script" + return [] unless File.exist?(JAR_RELATIVE) + + [DependencyFile.new( + name: JAR_RELATIVE, + content: Base64.encode64(File.binread(JAR_RELATIVE)), + content_encoding: DependencyFile::ContentEncoding::BASE64, + directory: wrapper_properties_file&.directory || "/" + )] + when "source" + return [] unless File.exist?(DOWNLOADER_RELATIVE) + + [DependencyFile.new( + name: DOWNLOADER_RELATIVE, + content: File.read(DOWNLOADER_RELATIVE), + directory: wrapper_properties_file&.directory || "/" + )] + else + [] + end + end + + # Memoized lookup of the maven-wrapper.properties DependencyFile. + sig { returns(T.nilable(DependencyFile)) } + def wrapper_properties_file + @wrapper_properties_file ||= T.let( + @dependency_files.find { |f| f.name.end_with?("maven-wrapper.properties") }, + T.nilable(Dependabot::DependencyFile) + ) + end + + # Writes all dependency files to the current working directory, + # base64-decoding binary files (e.g. the wrapper JAR) as needed. + sig { void } + def write_dependency_files + @dependency_files.each do |file| + FileUtils.mkdir_p(File.dirname(file.name)) + if file.content_encoding == DependencyFile::ContentEncoding::BASE64 + File.binwrite(file.name, Base64.decode64(T.must(file.content))) + else + File.write(file.name, file.content) + end + end + end + end + end + end +end diff --git a/maven/lib/dependabot/maven/native_helpers.rb b/maven/lib/dependabot/maven/native_helpers.rb index 6640a52ed42..694b6bfd1e3 100644 --- a/maven/lib/dependabot/maven/native_helpers.rb +++ b/maven/lib/dependabot/maven/native_helpers.rb @@ -1,9 +1,13 @@ # typed: strict # frozen_string_literal: true +require "fileutils" +require "open3" require "shellwords" +require "uri" require "sorbet-runtime" require "nokogiri" +require "dependabot/shared_helpers" module Dependabot module Maven @@ -46,6 +50,41 @@ def self.handle_tool_error(output) raise DependabotError, "mvn CLI failed with an unhandled error" end + + # Runs the Maven Wrapper plugin in the given directory to regenerate + # wrapper scripts and artifacts for the specified Maven distribution version. + # + # Plugin version strategy: + # Uses the fully-qualified coordinate + # org.apache.maven.plugins:maven-wrapper-plugin:VERSION:wrapper + # rather than the shorthand `wrapper:wrapper`. This pins the exact plugin + # version instead of relying on Maven's plugin prefix resolution, which + # varies by settings.xml and could silently use a different version. + sig do + params( + version: String, + wrapper_plugin_version: String, + env: T::Hash[String, String], + distribution_type: String, + extra_args: T::Array[String] + ).void + end + def self.run_mvnw_wrapper(version:, wrapper_plugin_version:, env:, distribution_type:, extra_args: []) + # Use the fully-qualified plugin goal so the exact plugin version is + # invoked regardless of the project's plugin group configuration. + plugin_goal = "org.apache.maven.plugins:maven-wrapper-plugin:" \ + "#{wrapper_plugin_version}:wrapper" + + standard_args = [ + plugin_goal, + "-Dmaven=#{version}", + "-Dtype=#{distribution_type}", + "--no-transfer-progress" + ] + extra_args + + cmd = Shellwords.join(["mvn"] + standard_args) + SharedHelpers.run_shell_command(cmd, env: env) + end end end end diff --git a/maven/lib/dependabot/maven/update_checker/requirements_updater.rb b/maven/lib/dependabot/maven/update_checker/requirements_updater.rb index 630e1acf5d7..9400fd73b02 100644 --- a/maven/lib/dependabot/maven/update_checker/requirements_updater.rb +++ b/maven/lib/dependabot/maven/update_checker/requirements_updater.rb @@ -10,6 +10,7 @@ require "dependabot/maven/update_checker" require "dependabot/maven/version" require "dependabot/maven/requirement" +require "dependabot/maven/distributions" module Dependabot module Maven @@ -53,6 +54,14 @@ def updated_requirements # requirement at index `i` to correspond to the previous requirement # at the same index. requirements.map do |req| + # Wrapper property requirements (distributionUrl, wrapperVersion, + # wrapperUrl) live in maven-wrapper.properties, not in a pom.xml. + # They must not go through POM XML update logic + # instead, only the version inside the requirement is updated. + if req.dig(:source, :type) == Distributions::DISTRIBUTION_DEPENDENCY_TYPE + next req.merge(requirement: T.must(latest_version).to_s) + end + next req if req.fetch(:requirement).nil? next req if req.fetch(:requirement).include?(",") diff --git a/maven/spec/dependabot/maven/file_fetcher_spec.rb b/maven/spec/dependabot/maven/file_fetcher_spec.rb index 3cd8f0143b6..010813395d1 100644 --- a/maven/spec/dependabot/maven/file_fetcher_spec.rb +++ b/maven/spec/dependabot/maven/file_fetcher_spec.rb @@ -639,4 +639,54 @@ end end end + + context "with a maven wrapper" do + before do + stub_request(:get, File.join(url, "pom.xml?ref=sha")) + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture("github", "contents_java_basic_pom.json"), + headers: { "content-type" => "application/json" } + ) + + stub_request(:get, File.join(url, ".mvn/extensions.xml?ref=sha")) + .with(headers: { "Authorization" => "token token" }) + .to_return(status: 404) + end + + context "when the maven_wrapper_updater experiment is disabled" do + before do + allow(Dependabot::Experiments).to receive(:enabled?).and_call_original + allow(Dependabot::Experiments).to receive(:enabled?) + .with(:maven_wrapper_updater).and_return(false) + + stub_request(:get, File.join(url, ".mvn/wrapper/maven-wrapper.properties?ref=sha")) + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture("github", "maven-wrapper.properties.json"), + headers: { "content-type" => "application/json" } + ) + end + + it "does not fetch wrapper files" do + expect(file_fetcher_instance.files.map(&:name)).to eq(%w(pom.xml)) + end + end + + context "when the maven_wrapper_updater experiment is enabled" do + before do + allow(Dependabot::Experiments).to receive(:enabled?).and_call_original + allow(Dependabot::Experiments).to receive(:enabled?) + .with(:maven_wrapper_updater).and_return(true) + end + + context "when maven-wrapper.properties is absent" do + it "fetches only pom.xml" do + expect(file_fetcher_instance.files.map(&:name)).to eq(%w(pom.xml)) + end + end + end + end end diff --git a/maven/spec/dependabot/maven/file_parser/wrapper_mojo_spec.rb b/maven/spec/dependabot/maven/file_parser/wrapper_mojo_spec.rb new file mode 100644 index 00000000000..b01d809b8c8 --- /dev/null +++ b/maven/spec/dependabot/maven/file_parser/wrapper_mojo_spec.rb @@ -0,0 +1,362 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency_file" +require "dependabot/maven/file_parser/wrapper_mojo" + +RSpec.describe Dependabot::Maven::FileParser::WrapperMojo do + def make_properties_file(name, content) + Dependabot::DependencyFile.new(name: name, content: content) + end + + def fixture_content(filename) + fixture("wrapper_files", filename) + end + + describe ".load_properties" do + subject(:props) { described_class.load_properties(content) } + + context "with only-script mode (≥ 3.3.4)" do + let(:content) { fixture_content("maven-wrapper-3.9.9-only-script.properties") } + + it "parses distributionUrl" do + expect(props.distribution_url).to eq( + "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip" + ) + end + + it "parses distributionSha256Sum" do + expect(props.distribution_sha256_sum).to eq("a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5cdbba7861a1498407") + end + + it "parses wrapperVersion" do + expect(props.wrapper_version).to eq("3.3.4") + end + + it "parses distributionType" do + expect(props.distribution_type).to eq("only-script") + end + end + + context "with bin mode (< 3.3.0)" do + let(:content) { fixture_content("maven-wrapper-3.9.6-bin.properties") } + + it "returns raw distribution_url with \\: escapes" do + expect(props.distribution_url).to eq( + "https\\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip" + ) + end + + it "parses wrapperUrl for wrapper version" do + expect(props.wrapper_version).to eq("3.2.0") + expect(props.wrapper_url).to include("maven-wrapper-3.2.0.jar") + end + + it "defaults distributionType to bin" do + expect(props.distribution_type).to eq("bin") + end + end + + context "with bin mode with checksum and explicit wrapperVersion" do + let(:content) { fixture_content("maven-wrapper-3.9.9-bin-checksum.properties") } + + it "prefers wrapperVersion over wrapperUrl" do + expect(props.wrapper_version).to eq("3.3.4") + end + end + + context "with a pre-release version in distributionUrl" do + let(:content) do + "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/" \ + "3.9.0-alpha-1/apache-maven-3.9.0-alpha-1-bin.zip\n" \ + "wrapperVersion=3.3.4\n" + end + + it "parses the pre-release version from distributionUrl" do + expect(props.distribution_url).to include("3.9.0-alpha-1") + end + end + + context "with all properties present" do + let(:dist_url) { "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip" } + let(:wrap_url) { "https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" } + let(:dist_sha) { "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5cdbba7861a1498407" } + let(:wrap_sha) { "e3b0c44298fc1c149afbf4c8996fb924" * 2 } + let(:content) do + "distributionUrl=#{dist_url}\n" \ + "distributionSha256Sum=#{dist_sha}\n" \ + "distributionType=bin\n" \ + "wrapperVersion=3.3.4\n" \ + "wrapperUrl=#{wrap_url}\n" \ + "wrapperSha256Sum=#{wrap_sha}\n" + end + + it "parses distributionUrl" do + expect(props.distribution_url).to eq(dist_url) + end + + it "parses distribution_version from the URL path" do + expect(props.distribution_version).to eq("3.9.9") + end + + it "parses distributionSha256Sum" do + expect(props.distribution_sha256_sum).to eq(dist_sha) + end + + it "parses distributionType" do + expect(props.distribution_type).to eq("bin") + end + + it "parses wrapperVersion" do + expect(props.wrapper_version).to eq("3.3.4") + end + + it "parses wrapperUrl" do + expect(props.wrapper_url).to include("maven-wrapper-3.3.4.jar") + end + + it "parses wrapperSha256Sum" do + expect(props.wrapper_sha256_sum).to eq(wrap_sha) + end + end + + context "with missing checksums" do + let(:base_url) { "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip" } + let(:content) { "distributionUrl=#{base_url}\ndistributionType=only-script\nwrapperVersion=3.3.4\n" } + + it "sets distribution_sha256_sum to nil" do + expect(props.distribution_sha256_sum).to be_nil + end + + it "sets wrapper_sha256_sum to nil" do + expect(props.wrapper_sha256_sum).to be_nil + end + end + + context "with missing distributionType and wrapperVersion" do + subject(:props) { described_class.load_properties(content, script_files: [mvnw_file]) } + + let(:content) do + "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip\n" + end + let(:script_content) { fixture("wrapper_files", "mvnw-3.3.0") } + let(:mvnw_file) { make_properties_file("mvnw", script_content) } + + it "defaults distributionType to bin" do + expect(props.distribution_type).to eq("bin") + end + + it "reads wrapper_version from the script file" do + expect(props.wrapper_version).to eq("3.3.0") + end + end + + context "when distributionUrl is missing" do + let(:content) { "wrapperVersion=3.3.4\n" } + + it "raises a missing mandatory property error" do + expect { props }.to raise_error(RuntimeError, /Missing mandatory property: distributionUrl/) + end + end + + context "when no wrapper version source is available" do + let(:content) do + "distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip\n" + end + + it "raises an error about the unresolvable wrapper version" do + expect { props }.to raise_error(RuntimeError, /Could not determine Maven Wrapper version/) + end + end + end + + describe ".extract_distribution_version" do + it "extracts the version from a standard bin zip URL" do + url = "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip" + expect(described_class.extract_distribution_version(url)).to eq("3.9.9") + end + + it "extracts an alpha pre-release version" do + url = "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.0-alpha-1/apache-maven-3.9.0-alpha-1-bin.zip" + expect(described_class.extract_distribution_version(url)).to eq("3.9.0-alpha-1") + end + + it "extracts an alpha pre-release version with a double-digit number" do + url = "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/4.0.0-alpha-10/apache-maven-4.0.0-alpha-10-bin.zip" + expect(described_class.extract_distribution_version(url)).to eq("4.0.0-alpha-10") + end + + it "extracts a beta pre-release version" do + url = "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/4.0.0-beta-3/apache-maven-4.0.0-beta-3-bin.zip" + expect(described_class.extract_distribution_version(url)).to eq("4.0.0-beta-3") + end + + it "extracts a release candidate version" do + url = "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/4.0.0-rc-2/apache-maven-4.0.0-rc-2-bin.zip" + expect(described_class.extract_distribution_version(url)).to eq("4.0.0-rc-2") + end + + it "extracts the version from a tar.gz URL" do + url = "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.tar.gz" + expect(described_class.extract_distribution_version(url)).to eq("3.8.6") + end + + it "raises when the URL contains no recognizable version path segment" do + expect { described_class.extract_distribution_version("https://example.com/some-artifact.zip") } + .to raise_error(RuntimeError, /Could not extract Maven version from content/) + end + end + + describe ".resolve_dependencies" do + let(:properties_file) { make_properties_file(".mvn/wrapper/maven-wrapper.properties", content) } + + context "with only-script mode" do + let(:content) { fixture_content("maven-wrapper-3.9.9-only-script.properties") } + + it "returns two dependencies" do + deps = described_class.resolve_dependencies(properties_file) + expect(deps.length).to eq(2) + end + + it "returns apache-maven dependency" do + dep = described_class.resolve_dependencies(properties_file) + .find { |d| d.name == "org.apache.maven:apache-maven" } + expect(dep).not_to be_nil + expect(dep.version).to eq("3.9.9") + expect(dep.requirements.first[:source][:type]).to eq("maven-distribution") + expect(dep.requirements.first[:source][:property]).to eq("distributionUrl") + end + + it "returns maven-wrapper dependency" do + dep = described_class.resolve_dependencies(properties_file) + .find { |d| d.name == "org.apache.maven.wrapper:maven-wrapper" } + expect(dep).not_to be_nil + expect(dep.version).to eq("3.3.4") + end + end + + context "with bin mode (< 3.3.0)" do + let(:content) { fixture_content("maven-wrapper-3.9.6-bin.properties") } + + it "returns maven-wrapper with wrapperUrl property" do + dep = described_class.resolve_dependencies(properties_file) + .find { |d| d.name == "org.apache.maven.wrapper:maven-wrapper" } + expect(dep).not_to be_nil + expect(dep.version).to eq("3.2.0") + end + end + + context "with 3.3.0 gap (no wrapperVersion, no wrapperUrl)" do + let(:content) { fixture_content("maven-wrapper-3.9.9-no-wrapper-version.properties") } + let(:script_content) { fixture("wrapper_files", "mvnw-3.3.0") } + let(:mvnw_file) { make_properties_file("mvnw", script_content) } + + it "falls back to script comment for wrapper version" do + deps = described_class.resolve_dependencies(properties_file, script_files: [mvnw_file]) + wrapper_dep = deps.find { |d| d.name == "org.apache.maven.wrapper:maven-wrapper" } + expect(wrapper_dep).not_to be_nil + expect(wrapper_dep.version).to eq("3.3.0") + end + end + + context "with source distribution type" do + let(:content) { fixture_content("maven-wrapper-3.9.9-source.properties") } + + it "returns two dependencies" do + deps = described_class.resolve_dependencies(properties_file) + expect(deps.length).to eq(2) + end + + it "returns the apache-maven dependency" do + dep = described_class.resolve_dependencies(properties_file) + .find { |d| d.name == "org.apache.maven:apache-maven" } + expect(dep).not_to be_nil + expect(dep.version).to eq("3.9.9") + end + + it "returns the maven-wrapper dependency" do + dep = described_class.resolve_dependencies(properties_file) + .find { |d| d.name == "org.apache.maven.wrapper:maven-wrapper" } + expect(dep).not_to be_nil + end + end + + context "when the distribution URL points to the Maven daemon (mvnd)" do + let(:content) do + "distributionUrl=https://archive.apache.org/dist/maven/mvnd/1.0.2/maven-mvnd-1.0.2-bin.zip\n" \ + "distributionType=bin\nwrapperVersion=3.3.4\n" + end + + it "returns an empty array" do + expect(described_class.resolve_dependencies(properties_file)).to eq([]) + end + + it "logs a warning that mvnd is not supported" do + expect(Dependabot.logger).to receive(:warn) + .with(/Maven daemon \(mvnd\) distribution is not supported/) + described_class.resolve_dependencies(properties_file) + end + end + + context "when the wrapperUrl points to a Takari distribution" do + let(:takari_url) { "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" } + let(:dist_url) do + "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip" + end + let(:content) { "distributionUrl=#{dist_url}\nwrapperUrl=#{takari_url}\n" } + + it "returns an empty array" do + expect(described_class.resolve_dependencies(properties_file)).to eq([]) + end + + it "logs a warning that Takari is not supported" do + expect(Dependabot.logger).to receive(:warn) + .with(/Takari distribution is not supported/) + described_class.resolve_dependencies(properties_file) + end + end + + context "with the apache-maven distribution dependency" do + let(:content) { fixture_content("maven-wrapper-3.9.9-only-script.properties") } + + it "sets packaging_type to pom in the requirement metadata" do + dep = described_class.resolve_dependencies(properties_file) + .find { |d| d.name == "org.apache.maven:apache-maven" } + dist_req = dep.requirements.find { |r| r[:source][:property] == "distributionUrl" } + expect(dist_req[:metadata]).to eq( + { + packaging_type: "pom", + wrapper_version: "3.3.4", + distribution_type: "only-script", + distribution_version: "3.9.9", + include_debug_script: false, + distribution_sha256_sum: "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5cdbba7861a1498407" + } + ) + end + end + + context "with wrapper checksum present" do + let(:dist_url) { "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip" } + let(:wrap_url) { "https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" } + let(:dist_sha) { "a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5cdbba7861a1498407" } + let(:wrap_sha) { "e3b0c44298fc1c149afbf4c8996fb924" * 2 } + let(:content) do + "distributionUrl=#{dist_url}\n" \ + "distributionSha256Sum=#{dist_sha}\n" \ + "wrapperVersion=3.3.4\n" \ + "wrapperUrl=#{wrap_url}\n" \ + "wrapperSha256Sum=#{wrap_sha}\n" + end + + it "captures wrapper_sha256_sum in the wrapperUrl requirement metadata" do + deps = described_class.resolve_dependencies(properties_file) + wrapper_req = deps.find { |d| d.requirements.any? { |r| r[:source][:property] == "wrapperUrl" } } + &.requirements + &.find { |r| r[:source][:property] == "wrapperUrl" } + expect(wrapper_req[:metadata]).to eq({ wrapper_sha256_sum: wrap_sha }) + end + end + end +end diff --git a/maven/spec/dependabot/maven/file_parser_spec.rb b/maven/spec/dependabot/maven/file_parser_spec.rb index 3f6c6df1f2d..0dd626d291c 100644 --- a/maven/spec/dependabot/maven/file_parser_spec.rb +++ b/maven/spec/dependabot/maven/file_parser_spec.rb @@ -1262,6 +1262,61 @@ end end + context "with maven wrapper files" do + let(:wrapper_content) { fixture("wrapper_files", "maven-wrapper-3.9.9-only-script.properties") } + let(:wrapper_file) do + Dependabot::DependencyFile.new( + name: ".mvn/wrapper/maven-wrapper.properties", + content: wrapper_content + ) + end + let(:files) { [pom, wrapper_file] } + + before do + allow(Dependabot::Experiments).to receive(:enabled?).and_return(false) + allow(Dependabot::Experiments).to receive(:enabled?) + .with(:maven_wrapper_updater).and_return(true) + end + + it "includes apache-maven as a dependency" do + expect(dependencies.map(&:name)).to include("org.apache.maven:apache-maven") + end + + it "includes maven-wrapper as a dependency" do + expect(dependencies.map(&:name)).to include("org.apache.maven.wrapper:maven-wrapper") + end + + it "sets the correct version for apache-maven" do + dep = dependencies.find { |d| d.name == "org.apache.maven:apache-maven" } + expect(dep.version).to eq("3.9.9") + end + + it "sets the correct version for maven-wrapper" do + dep = dependencies.find { |d| d.name == "org.apache.maven.wrapper:maven-wrapper" } + expect(dep.version).to eq("3.3.4") + end + + it "uses maven-distribution as the source type" do + dep = dependencies.find { |d| d.name == "org.apache.maven:apache-maven" } + expect(dep.requirements.first[:source][:type]).to eq("maven-distribution") + end + + context "when the maven_wrapper_updater experiment is disabled" do + before do + allow(Dependabot::Experiments).to receive(:enabled?) + .with(:maven_wrapper_updater).and_return(false) + end + + it "does not include apache-maven as a dependency" do + expect(dependencies.map(&:name)).not_to include("org.apache.maven:apache-maven") + end + + it "does not include maven-wrapper as a dependency" do + expect(dependencies.map(&:name)).not_to include("org.apache.maven.wrapper:maven-wrapper") + end + end + end + describe "#ecosystem" do subject(:ecosystem) { parser.ecosystem } diff --git a/maven/spec/dependabot/maven/file_updater/wrapper_updater_spec.rb b/maven/spec/dependabot/maven/file_updater/wrapper_updater_spec.rb new file mode 100644 index 00000000000..c3575b5ca86 --- /dev/null +++ b/maven/spec/dependabot/maven/file_updater/wrapper_updater_spec.rb @@ -0,0 +1,380 @@ +# typed: false +# frozen_string_literal: true + +require "base64" +require "spec_helper" +require "dependabot/dependency" +require "dependabot/dependency_file" +require "dependabot/maven/file_updater/wrapper_updater" +require "dependabot/maven/native_helpers" +require "dependabot/maven/distributions" +require "dependabot/shared_helpers" + +RSpec.describe Dependabot::Maven::FileUpdater::WrapperUpdater do + def make_file(name, content) + Dependabot::DependencyFile.new(name: name, content: content, directory: "/") + end + + def fixture_content(filename) + fixture("wrapper_files", filename) + end + + subject(:updater) do + described_class.new( + dependency_files: dependency_files, + dependency: dependency, + credentials: credentials + ) + end + + let(:properties_content) { fixture_content("maven-wrapper-3.9.9-only-script.properties") } + let(:properties_file) { make_file(".mvn/wrapper/maven-wrapper.properties", properties_content) } + let(:mvnw_file) do + make_file("mvnw", "#!/bin/sh\n# Apache Maven Wrapper startup script, version 3.3.4\nexec mvn \"$@\"\n") + end + let(:dependency_files) { [properties_file, mvnw_file] } + let(:credentials) { [] } + + let(:dependency) do + Dependabot::Dependency.new( + name: "org.apache.maven:apache-maven", + version: "3.9.9", + previous_version: "3.9.8", + requirements: [{ + requirement: "3.9.9", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + url: "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip", + property: "distributionUrl" + }, + groups: [], + metadata: { packaging_type: "pom", wrapper_version: "3.3.4", distribution_type: "only-script", + distribution_version: "3.9.9", include_debug_script: false } + }], + previous_requirements: [{ + requirement: "3.9.8", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + url: "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip", + property: "distributionUrl" + }, + groups: [], + metadata: { packaging_type: "pom", wrapper_version: "3.3.4", distribution_type: "only-script", + distribution_version: "3.9.8" } + }], + package_manager: "maven" + ) + end + + let(:buildfile) do + Dependabot::DependencyFile.new( + name: "pom.xml", + content: "", + directory: "/" + ) + end + + describe "#update_files" do + context "when dependency is not a wrapper dependency" do + let(:dependency) do + Dependabot::Dependency.new( + name: "com.google.guava:guava", + version: "32.0.0-jre", + previous_version: "31.0.0-jre", + requirements: [{ + requirement: "32.0.0-jre", + file: "pom.xml", + source: nil, + groups: [] + }], + previous_requirements: [{ + requirement: "31.0.0-jre", + file: "pom.xml", + source: nil, + groups: [] + }], + package_manager: "maven" + ) + end + + it "returns empty array" do + expect(updater.update_files(buildfile)).to eq([]) + end + end + + context "when no properties file is present" do + let(:dependency_files) { [mvnw_file] } + + it "returns empty array" do + expect(updater.update_files(buildfile)).to eq([]) + end + end + + # Shared setup for tests that drive the full update_files path. + # Stubs the native Maven command and provides HTTPS_PROXY which the + # env-builder reads unconditionally. + shared_context "with native helpers stubbed" do + around do |example| + saved = ENV.fetch("HTTPS_PROXY", nil) + ENV["HTTPS_PROXY"] = "http://proxy.example.test" + example.run + saved ? (ENV["HTTPS_PROXY"] = saved) : ENV.delete("HTTPS_PROXY") + end + + before do + allow(Dependabot::Maven::NativeHelpers).to receive(:run_mvnw_wrapper) + end + end + + context "when updating the distributionUrl" do + include_context "with native helpers stubbed" + + # Properties file starts at the OLD version so the gsub actually fires. + let(:old_dist_url) do + "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip" + end + let(:properties_content) do + "distributionUrl=#{old_dist_url}\ndistributionType=only-script\nwrapperVersion=3.3.4\n" + end + + it "returns a non-empty array of updated files" do + expect(updater.update_files(buildfile)).not_to be_empty + end + + it "includes the properties file" do + names = updater.update_files(buildfile).map(&:name) + expect(names).to include(".mvn/wrapper/maven-wrapper.properties") + end + end + + context "when updating the wrapperVersion" do + include_context "with native helpers stubbed" + + let(:dist_url) do + "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip" + end + let(:properties_content) do + "distributionUrl=#{dist_url}\ndistributionType=only-script\nwrapperVersion=3.3.3\n" + end + + let(:dependency) do + Dependabot::Dependency.new( + name: "org.apache.maven.wrapper:maven-wrapper", + version: "3.3.4", + previous_version: "3.3.3", + requirements: [ + { + requirement: "3.3.4", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + property: "wrapperVersion" + }, + groups: [], + metadata: { wrapper_version: "3.3.4", distribution_version: "3.9.9", distribution_type: "only-script", + include_debug_script: false } + }, + { + requirement: "3.9.9", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + property: "distributionUrl", + url: dist_url + }, + groups: [], + metadata: { packaging_type: "pom", wrapper_version: "3.3.4", distribution_type: "only-script", + distribution_version: "3.9.9", include_debug_script: false } + } + ], + previous_requirements: [ + { + requirement: "3.3.3", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + property: "wrapperVersion" + }, + groups: [], + metadata: { wrapper_version: "3.3.3", distribution_version: "3.9.9", include_debug_script: false } + }, + { + requirement: "3.9.9", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + property: "distributionUrl", + url: dist_url + }, + groups: [], + metadata: { packaging_type: "pom", wrapper_version: "3.3.4", distribution_type: "only-script", + distribution_version: "3.9.9", include_debug_script: false } + } + ], + package_manager: "maven" + ) + end + end + + context "when scripts are present in dependency_files" do + include_context "with native helpers stubbed" + + let(:old_dist_url) do + "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip" + end + let(:properties_content) do + "distributionUrl=#{old_dist_url}\ndistributionType=only-script\nwrapperVersion=3.3.4\n" + end + let(:mvnw_cmd_file) { make_file("mvnw.cmd", "@echo off\r\n") } + let(:dependency_files) { [properties_file, mvnw_file, mvnw_cmd_file] } + + it "includes the Unix script in the returned files" do + names = updater.update_files(buildfile).map(&:name) + expect(names).to include("mvnw") + end + + it "marks Unix scripts as EXECUTABLE" do + f = updater.update_files(buildfile).find { |file| file.name == "mvnw" } + expect(f&.mode).to eq(Dependabot::DependencyFile::Mode::EXECUTABLE) + end + + it "includes the Windows script in the returned files" do + names = updater.update_files(buildfile).map(&:name) + expect(names).to include("mvnw.cmd") + end + + it "does not mark Windows scripts as EXECUTABLE" do + f = updater.update_files(buildfile).find { |file| file.name == "mvnw.cmd" } + expect(f&.mode).not_to eq(Dependabot::DependencyFile::Mode::EXECUTABLE) + end + end + + context "with bin distribution type" do + include_context "with native helpers stubbed" + + let(:old_dist_url) do + "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip" + end + let(:properties_content) do + "distributionUrl=#{old_dist_url}\ndistributionType=bin\nwrapperVersion=3.3.4\n" + end + + let(:dependency) do + Dependabot::Dependency.new( + name: "org.apache.maven:apache-maven", + version: "3.9.9", + previous_version: "3.9.8", + requirements: [{ + requirement: "3.9.9", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + url: "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip", + property: "distributionUrl" + }, + groups: [], + metadata: { packaging_type: "pom", wrapper_version: "3.3.4", distribution_type: "bin", + distribution_version: "3.9.9", include_debug_script: false } + }], + previous_requirements: [{ + requirement: "3.9.8", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + url: old_dist_url, + property: "distributionUrl" + }, + groups: [], + metadata: { + packaging_type: "pom", + wrapper_version: "3.3.4", + distribution_type: "bin", + distribution_version: "3.9.8" + } + }], + package_manager: "maven" + ) + end + + let(:jar_file) do + f = make_file(".mvn/wrapper/maven-wrapper.jar", Base64.strict_encode64("fake-jar-bytes")) + f.content_encoding = Dependabot::DependencyFile::ContentEncoding::BASE64 + f + end + let(:dependency_files) { [properties_file, mvnw_file, jar_file] } + + it "includes the JAR file in the returned files" do + names = updater.update_files(buildfile).map(&:name) + expect(names).to include(".mvn/wrapper/maven-wrapper.jar") + end + + it "returns the JAR file with BASE64 content encoding" do + jar = updater.update_files(buildfile) + .find { |f| f.name == ".mvn/wrapper/maven-wrapper.jar" } + expect(jar&.content_encoding).to eq(Dependabot::DependencyFile::ContentEncoding::BASE64) + end + end + + context "with source distribution type" do + include_context "with native helpers stubbed" + + let(:old_dist_url) do + "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip" + end + let(:properties_content) do + "distributionUrl=#{old_dist_url}\ndistributionType=source\nwrapperVersion=3.3.4\n" + end + + let(:dependency) do + Dependabot::Dependency.new( + name: "org.apache.maven:apache-maven", + version: "3.9.9", + previous_version: "3.9.8", + requirements: [{ + requirement: "3.9.9", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + url: "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip", + property: "distributionUrl" + }, + groups: [], + metadata: { packaging_type: "pom", wrapper_version: "3.3.4", distribution_type: "source", + distribution_version: "3.9.9", include_debug_script: false } + }], + previous_requirements: [{ + requirement: "3.9.8", + file: ".mvn/wrapper/maven-wrapper.properties", + source: { + type: "maven-distribution", + url: old_dist_url, + property: "distributionUrl" + }, + groups: [], + metadata: { packaging_type: "pom", wrapper_version: "3.3.4", distribution_type: "source", + distribution_version: "3.9.8" } + }], + package_manager: "maven" + ) + end + + let(:downloader_content) { "public class MavenWrapperDownloader {}\n" } + let(:downloader_file) do + make_file(".mvn/wrapper/MavenWrapperDownloader.java", downloader_content) + end + let(:dependency_files) { [properties_file, mvnw_file, downloader_file] } + + it "includes MavenWrapperDownloader.java in the returned files" do + names = updater.update_files(buildfile).map(&:name) + expect(names).to include(".mvn/wrapper/MavenWrapperDownloader.java") + end + + it "does not include a JAR file" do + names = updater.update_files(buildfile).map(&:name) + expect(names).not_to include(".mvn/wrapper/maven-wrapper.jar") + end + end + end +end diff --git a/maven/spec/fixtures/github/MavenWrapperDownloader.java.json b/maven/spec/fixtures/github/MavenWrapperDownloader.java.json new file mode 100644 index 00000000000..abbac1b75d2 --- /dev/null +++ b/maven/spec/fixtures/github/MavenWrapperDownloader.java.json @@ -0,0 +1,18 @@ +{ + "name": "MavenWrapperDownloader.java", + "path": ".mvn/wrapper/MavenWrapperDownloader.java", + "sha": "ghi789jk0123ghi789jk0123ghi789jk0123ghi7", + "size": 5000, + "url": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/MavenWrapperDownloader.java?ref=master", + "html_url": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/MavenWrapperDownloader.java", + "git_url": "https://api.github.com/repos/gocardless/business/git/blobs/ghi789jk0123ghi789jk0123ghi789jk0123ghi7", + "download_url": "https://raw.githubusercontent.com/gocardless/business/master/.mvn/wrapper/MavenWrapperDownloader.java", + "type": "file", + "content": "cHVibGljIGNsYXNzIE1hdmVuV3JhcHBlckRvd25sb2FkZXIge30K\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/MavenWrapperDownloader.java?ref=master", + "git": "https://api.github.com/repos/gocardless/business/git/blobs/ghi789jk0123ghi789jk0123ghi789jk0123ghi7", + "html": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/MavenWrapperDownloader.java" + } +} diff --git a/maven/spec/fixtures/github/content_maven_wrapper.json b/maven/spec/fixtures/github/content_maven_wrapper.json new file mode 100644 index 00000000000..c34eae6602d --- /dev/null +++ b/maven/spec/fixtures/github/content_maven_wrapper.json @@ -0,0 +1,18 @@ +[ + { + "name": "maven-wrapper.properties", + "path": ".mvn/wrapper/maven-wrapper.properties", + "sha": "abc123def456abc123def456abc123def456abc1", + "size": 256, + "url": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/maven-wrapper.properties?ref=master", + "html_url": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/maven-wrapper.properties", + "git_url": "https://api.github.com/repos/gocardless/business/git/blobs/abc123def456abc123def456abc123def456abc1", + "download_url": "https://raw.githubusercontent.com/gocardless/business/master/.mvn/wrapper/maven-wrapper.properties", + "type": "file", + "_links": { + "self": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/maven-wrapper.properties?ref=master", + "git": "https://api.github.com/repos/gocardless/business/git/blobs/abc123def456abc123def456abc123def456abc1", + "html": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/maven-wrapper.properties" + } + } +] diff --git a/maven/spec/fixtures/github/contents_mvn_wrapper.json b/maven/spec/fixtures/github/contents_mvn_wrapper.json new file mode 100644 index 00000000000..c34eae6602d --- /dev/null +++ b/maven/spec/fixtures/github/contents_mvn_wrapper.json @@ -0,0 +1,18 @@ +[ + { + "name": "maven-wrapper.properties", + "path": ".mvn/wrapper/maven-wrapper.properties", + "sha": "abc123def456abc123def456abc123def456abc1", + "size": 256, + "url": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/maven-wrapper.properties?ref=master", + "html_url": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/maven-wrapper.properties", + "git_url": "https://api.github.com/repos/gocardless/business/git/blobs/abc123def456abc123def456abc123def456abc1", + "download_url": "https://raw.githubusercontent.com/gocardless/business/master/.mvn/wrapper/maven-wrapper.properties", + "type": "file", + "_links": { + "self": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/maven-wrapper.properties?ref=master", + "git": "https://api.github.com/repos/gocardless/business/git/blobs/abc123def456abc123def456abc123def456abc1", + "html": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/maven-wrapper.properties" + } + } +] diff --git a/maven/spec/fixtures/github/maven-wrapper.jar.json b/maven/spec/fixtures/github/maven-wrapper.jar.json new file mode 100644 index 00000000000..f592096f9ce --- /dev/null +++ b/maven/spec/fixtures/github/maven-wrapper.jar.json @@ -0,0 +1,18 @@ +{ + "name": "maven-wrapper.jar", + "path": ".mvn/wrapper/maven-wrapper.jar", + "sha": "fgh678ij9012fgh678ij9012fgh678ij9012fgh6", + "size": 51441, + "url": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/maven-wrapper.jar?ref=master", + "html_url": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/maven-wrapper.jar", + "git_url": "https://api.github.com/repos/gocardless/business/git/blobs/fgh678ij9012fgh678ij9012fgh678ij9012fgh6", + "download_url": "https://raw.githubusercontent.com/gocardless/business/master/.mvn/wrapper/maven-wrapper.jar", + "type": "file", + "content": "UEsDBBQAAAAIAAAAAAAAAAAAAAAAAAAAAAoAAABtZXRhLWluZg==\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/maven-wrapper.jar?ref=master", + "git": "https://api.github.com/repos/gocardless/business/git/blobs/fgh678ij9012fgh678ij9012fgh678ij9012fgh6", + "html": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/maven-wrapper.jar" + } +} diff --git a/maven/spec/fixtures/github/maven-wrapper.properties.json b/maven/spec/fixtures/github/maven-wrapper.properties.json new file mode 100644 index 00000000000..ceb905528b3 --- /dev/null +++ b/maven/spec/fixtures/github/maven-wrapper.properties.json @@ -0,0 +1,18 @@ +{ + "name": "maven-wrapper.properties", + "path": ".mvn/wrapper/maven-wrapper.properties", + "sha": "abc123def456abc123def456abc123def456abc1", + "size": 256, + "url": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/maven-wrapper.properties?ref=master", + "html_url": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/maven-wrapper.properties", + "git_url": "https://api.github.com/repos/gocardless/business/git/blobs/abc123def456abc123def456abc123def456abc1", + "download_url": "https://raw.githubusercontent.com/gocardless/business/master/.mvn/wrapper/maven-wrapper.properties", + "type": "file", + "content": "ZGlzdHJpYnV0aW9uVXJsPWh0dHBzOi8vcmVwby5tYXZlbi5hcGFjaGUub3JnL21hdmVuMi9vcmcv\nYXBhY2hlL21hdmVuL2FwYWNoZS1tYXZlbi8zLjkuOS9hcGFjaGUtbWF2ZW4tMy45LjktYmluLnpp\ncApkaXN0cmlidXRpb25TaGEyNTZTdW09YTU1NTI1NGQ2YjUzZDI2Nzk2NWEzNDA0ZWNiMTRlNTNj\nMzgyN2MwOWMzYjk0YjVjZGJiYTc4NjFhMTQ5ODQwNwpkaXN0cmlidXRpb25UeXBlPW9ubHktc2Ny\naXB0CndyYXBwZXJWZXJzaW9uPTMuMy40Cg==\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/gocardless/business/contents/.mvn/wrapper/maven-wrapper.properties?ref=master", + "git": "https://api.github.com/repos/gocardless/business/git/blobs/abc123def456abc123def456abc123def456abc1", + "html": "https://github.com/gocardless/business/blob/master/.mvn/wrapper/maven-wrapper.properties" + } +} diff --git a/maven/spec/fixtures/github/mvnw.cmd.json b/maven/spec/fixtures/github/mvnw.cmd.json new file mode 100644 index 00000000000..a1398ec1f57 --- /dev/null +++ b/maven/spec/fixtures/github/mvnw.cmd.json @@ -0,0 +1,18 @@ +{ + "name": "mvnw.cmd", + "path": "mvnw.cmd", + "sha": "cde345fg6789cde345fg6789cde345fg6789cde3", + "size": 64, + "url": "https://api.github.com/repos/gocardless/business/contents/mvnw.cmd?ref=master", + "html_url": "https://github.com/gocardless/business/blob/master/mvnw.cmd", + "git_url": "https://api.github.com/repos/gocardless/business/git/blobs/cde345fg6789cde345fg6789cde345fg6789cde3", + "download_url": "https://raw.githubusercontent.com/gocardless/business/master/mvnw.cmd", + "type": "file", + "content": "QFJFTSBBcGFjaGUgTWF2ZW4gV3JhcHBlciBzdGFydHVwIGJhdGNoIHNjcmlwdCwgdmVyc2lvbiAz\nLjMuNAptdm4gJSoK\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/gocardless/business/contents/mvnw.cmd?ref=master", + "git": "https://api.github.com/repos/gocardless/business/git/blobs/cde345fg6789cde345fg6789cde345fg6789cde3", + "html": "https://github.com/gocardless/business/blob/master/mvnw.cmd" + } +} diff --git a/maven/spec/fixtures/github/mvnw.json b/maven/spec/fixtures/github/mvnw.json new file mode 100644 index 00000000000..27988b707d0 --- /dev/null +++ b/maven/spec/fixtures/github/mvnw.json @@ -0,0 +1,18 @@ +{ + "name": "mvnw", + "path": "mvnw", + "sha": "bcd234ef5678bcd234ef5678bcd234ef5678bcd2", + "size": 72, + "url": "https://api.github.com/repos/gocardless/business/contents/mvnw?ref=master", + "html_url": "https://github.com/gocardless/business/blob/master/mvnw", + "git_url": "https://api.github.com/repos/gocardless/business/git/blobs/bcd234ef5678bcd234ef5678bcd234ef5678bcd2", + "download_url": "https://raw.githubusercontent.com/gocardless/business/master/mvnw", + "type": "file", + "content": "IyEvYmluL3NoCiMgQXBhY2hlIE1hdmVuIFdyYXBwZXIgc3RhcnR1cCBzY3JpcHQsIHZlcnNpb24g\nMy4zLjQKZXhlYyBtdm4gIiRAIgo=\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/gocardless/business/contents/mvnw?ref=master", + "git": "https://api.github.com/repos/gocardless/business/git/blobs/bcd234ef5678bcd234ef5678bcd234ef5678bcd2", + "html": "https://github.com/gocardless/business/blob/master/mvnw" + } +} diff --git a/maven/spec/fixtures/github/mvnwDebug.cmd.json b/maven/spec/fixtures/github/mvnwDebug.cmd.json new file mode 100644 index 00000000000..9bd400e742c --- /dev/null +++ b/maven/spec/fixtures/github/mvnwDebug.cmd.json @@ -0,0 +1,18 @@ +{ + "name": "mvnwDebug.cmd", + "path": "mvnwDebug.cmd", + "sha": "efg567hi8901efg567hi8901efg567hi8901efg5", + "size": 72, + "url": "https://api.github.com/repos/gocardless/business/contents/mvnwDebug.cmd?ref=master", + "html_url": "https://github.com/gocardless/business/blob/master/mvnwDebug.cmd", + "git_url": "https://api.github.com/repos/gocardless/business/git/blobs/efg567hi8901efg567hi8901efg567hi8901efg5", + "download_url": "https://raw.githubusercontent.com/gocardless/business/master/mvnwDebug.cmd", + "type": "file", + "content": "QFJFTSBBcGFjaGUgTWF2ZW4gV3JhcHBlciBkZWJ1ZyBiYXRjaCBzY3JpcHQsIHZlcnNpb24gMy4z\nLjQKbXZuICUqCg==\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/gocardless/business/contents/mvnwDebug.cmd?ref=master", + "git": "https://api.github.com/repos/gocardless/business/git/blobs/efg567hi8901efg567hi8901efg567hi8901efg5", + "html": "https://github.com/gocardless/business/blob/master/mvnwDebug.cmd" + } +} diff --git a/maven/spec/fixtures/github/mvnwDebug.json b/maven/spec/fixtures/github/mvnwDebug.json new file mode 100644 index 00000000000..b332389a0e7 --- /dev/null +++ b/maven/spec/fixtures/github/mvnwDebug.json @@ -0,0 +1,18 @@ +{ + "name": "mvnwDebug", + "path": "mvnwDebug", + "sha": "def456gh7890def456gh7890def456gh7890def4", + "size": 80, + "url": "https://api.github.com/repos/gocardless/business/contents/mvnwDebug?ref=master", + "html_url": "https://github.com/gocardless/business/blob/master/mvnwDebug", + "git_url": "https://api.github.com/repos/gocardless/business/git/blobs/def456gh7890def456gh7890def456gh7890def4", + "download_url": "https://raw.githubusercontent.com/gocardless/business/master/mvnwDebug", + "type": "file", + "content": "IyEvYmluL3NoCiMgQXBhY2hlIE1hdmVuIFdyYXBwZXIgZGVidWcgc3RhcnR1cCBzY3JpcHQsIHZl\ncnNpb24gMy4zLjQKZXhlYyBtdm4gIiRAIgo=\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/gocardless/business/contents/mvnwDebug?ref=master", + "git": "https://api.github.com/repos/gocardless/business/git/blobs/def456gh7890def456gh7890def456gh7890def4", + "html": "https://github.com/gocardless/business/blob/master/mvnwDebug" + } +} diff --git a/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.6-bin.properties b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.6-bin.properties new file mode 100644 index 00000000000..28df8b51fcf --- /dev/null +++ b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.6-bin.properties @@ -0,0 +1,2 @@ +distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-bin-checksum.properties b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-bin-checksum.properties new file mode 100644 index 00000000000..76eadb90c77 --- /dev/null +++ b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-bin-checksum.properties @@ -0,0 +1,5 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionSha256Sum=a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5cdbba7861a1498407 +distributionType=bin +wrapperVersion=3.3.4 +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar diff --git a/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-no-wrapper-version.properties b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-no-wrapper-version.properties new file mode 100644 index 00000000000..a456c84e492 --- /dev/null +++ b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-no-wrapper-version.properties @@ -0,0 +1,3 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionSha256Sum=a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5cdbba7861a1498407 +distributionType=only-script diff --git a/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-only-script.properties b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-only-script.properties new file mode 100644 index 00000000000..2cf24793f14 --- /dev/null +++ b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-only-script.properties @@ -0,0 +1,4 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionSha256Sum=a555254d6b53d267965a3404ecb14e53c3827c09c3b94b5cdbba7861a1498407 +distributionType=only-script +wrapperVersion=3.3.4 diff --git a/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-source.properties b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-source.properties new file mode 100644 index 00000000000..342d66574f1 --- /dev/null +++ b/maven/spec/fixtures/wrapper_files/maven-wrapper-3.9.9-source.properties @@ -0,0 +1,3 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip +distributionType=source +wrapperVersion=3.3.4 diff --git a/maven/spec/fixtures/wrapper_files/mvnw-3.3.0 b/maven/spec/fixtures/wrapper_files/mvnw-3.3.0 new file mode 100644 index 00000000000..022cc433fbf --- /dev/null +++ b/maven/spec/fixtures/wrapper_files/mvnw-3.3.0 @@ -0,0 +1,12 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup script, version 3.3.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# ----------------------------------------------------------------------------