Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 15 additions & 33 deletions gradle/lib/dependabot/gradle/package/package_details_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
require "dependabot/maven/utils/auth_headers_finder"
require "sorbet-runtime"
require "dependabot/gradle/metadata_finder"
require "dependabot/gradle/package/release_date_extractor"

module Dependabot
module Gradle
Expand Down Expand Up @@ -101,37 +102,16 @@ def fetch_available_versions

sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
def release_details
release_date_info = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]])

begin
repositories.map do |repository_details|
url = repository_details.fetch("url")
next unless url == Gradle::FileParser::RepositoriesFinder::CENTRAL_REPO_URL

release_info_metadata(repository_details).css("a[title]").each do |link|
version_string = link["title"]
version = version_string.gsub(%r{/$}, "")
raw_date_text = link.next.text.strip.split("\n").last.strip

release_date = begin
Time.parse(raw_date_text)
rescue StandardError
nil
end

next unless version && version_class.correct?(version)

release_date_info[version] = {
release_date: release_date
}
end
end
extractor = ReleaseDateExtractor.new(
dependency_name: dependency.name,
version_class: version_class
)

release_date_info
rescue StandardError
Dependabot.logger.error("Failed to get release date")
{}
end
extractor.extract(
repositories: repositories,
dependency_metadata_fetcher: ->(repo) { dependency_metadata(repo) },
release_info_metadata_fetcher: ->(repo) { release_info_metadata(repo) }
)
end

sig { returns(T::Array[T::Hash[String, T.untyped]]) }
Expand Down Expand Up @@ -226,6 +206,8 @@ def dependency_metadata(repository_details)
end
end

# Fetches HTML directory listing from Maven-compatible repositories.
# Uses CSS selector "a[title]" to extract versions and dates. Caches results per repository.
sig { params(repository_details: T::Hash[T.untyped, T.untyped]).returns(T.untyped) }
def release_info_metadata(repository_details)
@release_info_metadata ||= T.let({}, T.nilable(T::Hash[Integer, T.untyped]))
Expand All @@ -237,14 +219,14 @@ def release_info_metadata(repository_details)
)

check_response(response, repository_details.fetch("url"))
Nokogiri::XML(response.body)
Nokogiri::HTML(response.body)
rescue URI::InvalidURIError
Nokogiri::XML("")
Nokogiri::HTML("")
rescue Excon::Error::Socket, Excon::Error::Timeout,
Excon::Error::TooManyRedirects
raise if central_repo_urls.include?(repository_details["url"])

Nokogiri::XML("")
Nokogiri::HTML("")
end
end

Expand Down
166 changes: 166 additions & 0 deletions gradle/lib/dependabot/gradle/package/release_date_extractor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# typed: strict
# frozen_string_literal: true

require "nokogiri"
require "time"
require "sorbet-runtime"
require "dependabot/logger"

module Dependabot
module Gradle
module Package
# Extracts release dates from repository metadata to support the cooldown feature.
# Handles multiple repository formats (Maven Central HTML listings, Gradle Plugin Portal XML).
class ReleaseDateExtractor
extend T::Sig

sig do
params(
dependency_name: String,
version_class: T.class_of(Dependabot::Version)
).void
end
def initialize(dependency_name:, version_class:)
@dependency_name = dependency_name
@version_class = version_class
end

# Extracts release dates from all repositories.
# Attempts both parsing strategies for all repositories:
# 1. Gradle Plugin Portal style: maven-metadata.xml with lastUpdated timestamp (latest version only)
# 2. Maven repository style: HTML directory listings with per-version dates
# This supports mirrors/proxies of both Maven Central and Gradle Plugin Portal.
sig do
params(
repositories: T::Array[T::Hash[String, T.untyped]],
dependency_metadata_fetcher: T.proc.params(
repo: T::Hash[String, T.untyped]
).returns(Nokogiri::XML::Document),
release_info_metadata_fetcher: T.proc.params(
repo: T::Hash[String, T.untyped]
).returns(Nokogiri::HTML::Document)
).returns(T::Hash[String, T::Hash[Symbol, T.untyped]])
end
def extract(repositories:, dependency_metadata_fetcher:, release_info_metadata_fetcher:)
release_date_info = T.let({}, T::Hash[String, T::Hash[Symbol, T.untyped]])

begin
repositories.each do |repository_details|
parse_gradle_plugin_portal_release(
repository_details,
release_date_info,
dependency_metadata_fetcher
)

parse_maven_central_releases(
repository_details,
release_date_info,
release_info_metadata_fetcher
)
end

release_date_info
rescue StandardError => e
Dependabot.logger.error(
"Failed to get release date for #{@dependency_name}: #{e.class} - #{e.message}"
)
Dependabot.logger.error(e.backtrace&.join("\n") || "No backtrace available")
{}
end
end

private

sig { returns(String) }
attr_reader :dependency_name

sig { returns(T.class_of(Dependabot::Version)) }
attr_reader :version_class

# Parses Maven-style HTML directory listings to extract release dates.
sig do
params(
repository_details: T::Hash[String, T.untyped],
release_date_info: T::Hash[String, T::Hash[Symbol, T.untyped]],
metadata_fetcher: T.proc.params(
repo: T::Hash[String, T.untyped]
).returns(Nokogiri::HTML::Document)
).void
end
def parse_maven_central_releases(repository_details, release_date_info, metadata_fetcher)
metadata_fetcher.call(repository_details).css("a[title]").each do |link|
title = link["title"]
next unless title

version = title.gsub(%r{/$}, "")
next unless version_class.correct?(version)
next if release_date_info.key?(version)

release_date = extract_release_date_from_link(link, version)
release_date_info[version] = { release_date: release_date }
end
rescue StandardError => e
Dependabot.logger.debug(
"Could not parse Maven-style release dates from #{repository_details.fetch('url')} " \
"for #{dependency_name}: #{e.message}"
)
end

# Parses Gradle Plugin Portal maven-metadata.xml for release dates.
sig do
params(
repository_details: T::Hash[String, T.untyped],
release_date_info: T::Hash[String, T::Hash[Symbol, T.untyped]],
metadata_fetcher: T.proc.params(
repo: T::Hash[String, T.untyped]
).returns(Nokogiri::XML::Document)
).void
end
def parse_gradle_plugin_portal_release(repository_details, release_date_info, metadata_fetcher)
metadata_xml = metadata_fetcher.call(repository_details)
last_updated = metadata_xml.at_xpath("//metadata/versioning/lastUpdated")&.text&.strip
latest_version = metadata_xml.at_xpath("//metadata/versioning/latest")&.text&.strip

return unless latest_version && version_class.correct?(latest_version)
return if release_date_info.key?(latest_version)

release_date = parse_gradle_timestamp(last_updated)
Dependabot.logger.info(
"Parsed Gradle Plugin Portal release for #{dependency_name}: #{latest_version} at #{release_date}"
)
release_date_info[latest_version] = { release_date: release_date }
rescue StandardError => e
Dependabot.logger.debug(
"Could not parse Gradle Plugin Portal metadata from #{repository_details.fetch('url')} " \
"for #{dependency_name}: #{e.message}"
)
end

# Extracts release date from HTML link element's adjacent text.
sig { params(link: Nokogiri::XML::Element, version: String).returns(T.nilable(Time)) }
def extract_release_date_from_link(link, version)
raw_date_text = link.next.text.strip.split("\n").last.strip
Time.parse(raw_date_text)
rescue StandardError => e
Dependabot.logger.debug(
"Failed to parse release date for #{dependency_name} version #{version}: #{e.message}"
)
nil
end

# Parses Gradle Plugin Portal timestamp format (YYYYMMDDHHmmss).
sig { params(timestamp: T.nilable(String)).returns(T.nilable(Time)) }
def parse_gradle_timestamp(timestamp)
return nil if timestamp.nil? || timestamp.empty?

Time.strptime(timestamp, "%Y%m%d%H%M%S")
rescue ArgumentError => e
Dependabot.logger.warn(
"Failed to parse Gradle timestamp for #{dependency_name}: '#{timestamp}' - #{e.message}"
)
nil
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,24 @@
"https://repo.maven.apache.org/maven2/org/springframework/boot/" \
"org.springframework.boot.gradle.plugin/maven-metadata.xml"
end
let(:maven_central_html_url) do
"https://repo.maven.apache.org/maven2/org/springframework/boot/" \
"org.springframework.boot.gradle.plugin/"
end

before do
stub_request(:get, gradle_plugin_metadata_url)
.to_return(status: 200, body: gradle_plugin_releases)
stub_request(:get, maven_metadata_url).to_return(status: 404)
# Stub the HTML directory listing request for Maven Central
stub_request(:get, maven_central_html_url).to_return(status: 404)
end

it "populates release_details for the latest version" do
release_info = packagedetailsfetcher.send(:release_details)
expect(release_info).to be_a(Hash)
expect(release_info).to have_key("2.1.4.RELEASE")
expect(release_info["2.1.4.RELEASE"][:release_date]).to eq(Time.utc(2019, 4, 4, 5, 30, 33))
end

describe "the first version" do
Expand All @@ -135,6 +148,11 @@
its([:source_url]) do
is_expected.to eq("https://plugins.gradle.org/m2")
end

its([:released_at]) do
# lastUpdated from fixture: 20190404053033 (2019-04-04 05:30:33 UTC)
is_expected.to eq(Time.utc(2019, 4, 4, 5, 30, 33))
end
end
end

Expand All @@ -161,11 +179,17 @@
"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/jvm/" \
"org.jetbrains.kotlin.jvm.gradle.plugin/maven-metadata.xml"
end
let(:maven_central_html_url) do
"https://repo.maven.apache.org/maven2/org/jetbrains/kotlin/jvm/" \
"org.jetbrains.kotlin.jvm.gradle.plugin/"
end

before do
stub_request(:get, gradle_plugin_metadata_url)
.to_return(status: 200, body: gradle_plugin_releases)
stub_request(:get, maven_metadata_url).to_return(status: 404)
# Stub the HTML directory listing request for Maven Central
stub_request(:get, maven_central_html_url).to_return(status: 404)
end

describe "the first version" do
Expand All @@ -190,6 +214,11 @@
its([:source_url]) do
is_expected.to eq("https://plugins.gradle.org/m2")
end

its([:released_at]) do
# lastUpdated from fixture: 20201222143435 (2020-12-22 14:34:35 UTC)
is_expected.to eq(Time.utc(2020, 12, 22, 14, 34, 35))
end
end
end

Expand Down
Loading
Loading