diff --git a/Library/Homebrew/livecheck/livecheck.rb b/Library/Homebrew/livecheck/livecheck.rb index 953126526055e..d27c6b51e4975 100644 --- a/Library/Homebrew/livecheck/livecheck.rb +++ b/Library/Homebrew/livecheck/livecheck.rb @@ -25,6 +25,11 @@ module Livecheck lolg.it ].freeze + STRATEGY_SYMBOLS_TO_SKIP_PREPROCESS_URL = [ + :github_latest, + :page_match, + ].freeze + UNSTABLE_VERSION_KEYWORDS = %w[ alpha beta @@ -381,14 +386,18 @@ def latest_version(formula, args:) next end - # Do not preprocess the URL when livecheck.strategy is set to :page_match - url = if livecheck_strategy == :page_match + # Only preprocess the URL when it's appropriate + url = if STRATEGY_SYMBOLS_TO_SKIP_PREPROCESS_URL.include?(livecheck_strategy) original_url else preprocess_url(original_url) end - strategies = Strategy.from_url(url, livecheck_regex.present?) + strategies = Strategy.from_url( + url, + livecheck_strategy: livecheck_strategy, + regex_provided: livecheck_regex.present?, + ) strategy = Strategy.from_symbol(livecheck_strategy) strategy ||= strategies.first strategy_name = @livecheck_strategy_names[strategy] diff --git a/Library/Homebrew/livecheck/strategy.rb b/Library/Homebrew/livecheck/strategy.rb index 766512e08f659..c2c6f5ca40f0b 100644 --- a/Library/Homebrew/livecheck/strategy.rb +++ b/Library/Homebrew/livecheck/strategy.rb @@ -52,19 +52,28 @@ def from_symbol(symbol) # Returns an array of strategies that apply to the provided URL. # # @param url [String] the URL to check for matching strategies - # @param regex_provided [Boolean] whether a regex is provided in a + # @param livecheck_strategy [Symbol] a {Strategy} symbol from the + # `livecheck` block + # @param regex_provided [Boolean] whether a regex is provided in the # `livecheck` block # @return [Array] - def from_url(url, regex_provided = nil) + def from_url(url, livecheck_strategy: nil, regex_provided: nil) usable_strategies = strategies.values.select do |strategy| - # Ignore strategies with a priority of 0 or lower - next if strategy.const_defined?(:PRIORITY) && !strategy::PRIORITY.positive? + if strategy == PageMatch + # Only treat the `PageMatch` strategy as usable if a regex is + # present in the `livecheck` block + next unless regex_provided + elsif strategy.const_defined?(:PRIORITY) && + !strategy::PRIORITY.positive? && + from_symbol(livecheck_strategy) != strategy + # Ignore strategies with a priority of 0 or lower, unless the + # strategy is specified in the `livecheck` block + next + end strategy.respond_to?(:match?) && strategy.match?(url) end - usable_strategies << strategies[:page_match] if strategies.key?(:page_match) && regex_provided - # Sort usable strategies in descending order by priority, using the # DEFAULT_PRIORITY when a strategy doesn't contain a PRIORITY constant usable_strategies.sort_by do |strategy| @@ -78,6 +87,7 @@ def from_url(url, regex_provided = nil) require_relative "strategy/apache" require_relative "strategy/bitbucket" require_relative "strategy/git" +require_relative "strategy/github_latest" require_relative "strategy/gnome" require_relative "strategy/gnu" require_relative "strategy/hackage" diff --git a/Library/Homebrew/livecheck/strategy/github_latest.rb b/Library/Homebrew/livecheck/strategy/github_latest.rb new file mode 100644 index 0000000000000..d3afcddbf4700 --- /dev/null +++ b/Library/Homebrew/livecheck/strategy/github_latest.rb @@ -0,0 +1,73 @@ +# typed: false +# frozen_string_literal: true + +module Homebrew + module Livecheck + module Strategy + # The {GithubLatest} strategy identifies versions of software at + # github.com by checking a repository's "latest" release page. + # + # GitHub URLs take a few different formats: + # + # * `https://github.com/example/example/releases/download/1.2.3/example-1.2.3.tar.gz` + # * `https://github.com/example/example/archive/v1.2.3.tar.gz` + # * `https://github.com/downloads/example/example/example-1.2.3.tar.gz` + # + # A repository's `/releases/latest` URL normally redirects to a release + # tag (e.g., `/releases/tag/1.2.3`). When there isn't a "latest" release, + # it will redirect to the `/releases` page. + # + # This strategy should only be used when we know the upstream repository + # has a "latest" release and the tagged release is appropriate to use + # (e.g., "latest" isn't wrongly pointing to an unstable version, not + # picking up the actual latest version, etc.). The strategy can only be + # applied by using `strategy :github_latest` in a `livecheck` block. + # + # The default regex identifies versions like `1.2.3`/`v1.2.3` in `href` + # attributes containing the tag URL (e.g., + # `/example/example/releases/tag/v1.2.3`). This is a common tag format + # but a modified regex can be provided in a `livecheck` block to override + # the default if a repository uses a different format (e.g., + # `example-1.2.3`, `1.2.3d`, `1.2.3-4`, etc.). + # + # @api public + class GithubLatest + NICE_NAME = "GitHub - Latest" + + # A priority of zero causes livecheck to skip the strategy. We do this + # for {GithubLatest} so we can selectively apply the strategy using + # `strategy :github_latest` in a `livecheck` block. + PRIORITY = 0 + + # The `Regexp` used to determine if the strategy applies to the URL. + URL_MATCH_REGEX = %r{//github\.com(?:/downloads)?(?:/[^/]+){2}}i.freeze + + # Whether the strategy can be applied to the provided URL. + # + # @param url [String] the URL to match against + # @return [Boolean] + def self.match?(url) + URL_MATCH_REGEX.match?(url) + end + + # Generates a URL and regex (if one isn't provided) and passes them + # to {PageMatch.find_versions} to identify versions in the content. + # + # @param url [String] the URL of the content to check + # @param regex [Regexp] a regex used for matching versions in content + # @return [Hash] + def self.find_versions(url, regex = nil) + %r{github\.com/(?:downloads/)?(?[^/]+)/(?[^/]+)}i =~ url.sub(/\.git$/i, "") + + # Example URL: `https://github.com/example/example/releases/latest` + page_url = "https://github.com/#{username}/#{repository}/releases/latest" + + # The default regex is the same for all URLs using this strategy + regex ||= %r{href=.*?/tag/v?(\d+(?:\.\d+)+)["' >]}i + + Homebrew::Livecheck::Strategy::PageMatch.find_versions(page_url, regex) + end + end + end + end +end diff --git a/Library/Homebrew/test/livecheck/strategy/github_latest_spec.rb b/Library/Homebrew/test/livecheck/strategy/github_latest_spec.rb new file mode 100644 index 0000000000000..d15088049af63 --- /dev/null +++ b/Library/Homebrew/test/livecheck/strategy/github_latest_spec.rb @@ -0,0 +1,33 @@ +# typed: false +# frozen_string_literal: true + +require "livecheck/strategy/github_latest" + +describe Homebrew::Livecheck::Strategy::GithubLatest do + subject(:github_latest) { described_class } + + let(:github_release_artifact_url) { + "https://github.com/example/example/releases/download/1.2.3/example-1.2.3.tar.gz" + } + let(:github_tag_archive_url) { "https://github.com/example/example/archive/v1.2.3.tar.gz" } + let(:github_repository_upload_url) { "https://github.com/downloads/example/example/example-1.2.3.tar.gz" } + let(:non_github_url) { "https://brew.sh/test" } + + describe "::match?" do + it "returns true if the argument provided is a GitHub release artifact URL" do + expect(github_latest.match?(github_release_artifact_url)).to be true + end + + it "returns true if the argument provided is a GitHub tag archive URL" do + expect(github_latest.match?(github_tag_archive_url)).to be true + end + + it "returns true if the argument provided is a GitHub repository upload URL" do + expect(github_latest.match?(github_repository_upload_url)).to be true + end + + it "returns false if the argument provided is not a GitHub URL" do + expect(github_latest.match?(non_github_url)).to be false + end + end +end