From 00e04924089aaeba616ed978168a2684b6d372f4 Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Tue, 18 Apr 2023 15:42:26 +0200 Subject: [PATCH] feat: add github_release strategy --- Library/Homebrew/livecheck/strategy.rb | 1 + .../livecheck/strategy/github_latest.rb | 37 +++-- .../livecheck/strategy/github_release.rb | 136 ++++++++++++++++++ 3 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 Library/Homebrew/livecheck/strategy/github_release.rb diff --git a/Library/Homebrew/livecheck/strategy.rb b/Library/Homebrew/livecheck/strategy.rb index dc5446d160cd09..9f8f4b7d4c0493 100644 --- a/Library/Homebrew/livecheck/strategy.rb +++ b/Library/Homebrew/livecheck/strategy.rb @@ -273,6 +273,7 @@ def self.handle_block_return(value) require_relative "strategy/extract_plist" require_relative "strategy/git" require_relative "strategy/github_latest" +require_relative "strategy/github_release" 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 index d91f5074b40cc3..3384af78bcb75a 100644 --- a/Library/Homebrew/livecheck/strategy/github_latest.rb +++ b/Library/Homebrew/livecheck/strategy/github_latest.rb @@ -41,24 +41,13 @@ class GithubLatest # `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{ - ^https?://github\.com - /(?:downloads/)?(?[^/]+) # The GitHub username - /(?[^/]+) # The GitHub repository name - }ix.freeze - - # The default regex used to identify a version from a tag when a regex - # isn't provided. - DEFAULT_REGEX = %r{href=.*?/tag/v?(\d+(?:\.\d+)+)["' >]}i.freeze - # Whether the strategy can be applied to the provided URL. # # @param url [String] the URL to match against # @return [Boolean] sig { params(url: String).returns(T::Boolean) } def self.match?(url) - URL_MATCH_REGEX.match?(url) + GithubRelease::URL_MATCH_REGEX.match?(url) end # Extracts information from a provided URL and uses it to generate @@ -72,11 +61,13 @@ def self.match?(url) def self.generate_input_values(url) values = {} - match = url.sub(/\.git$/i, "").match(URL_MATCH_REGEX) + match = url.sub(/\.git$/i, "").match(GithubRelease::URL_MATCH_REGEX) return values if match.blank? # Example URL: `https://github.com/example/example/releases/latest` values[:url] = "https://github.com/#{match[:username]}/#{match[:repository]}/releases/latest" + values[:username] = match[:username] + values[:repository] = match[:repository] values end @@ -89,16 +80,24 @@ def self.generate_input_values(url) # @return [Hash] sig { params( - url: String, - regex: T.nilable(Regexp), - unused: T.nilable(T::Hash[Symbol, T.untyped]), - block: T.nilable(Proc), + url: String, + regex: T.nilable(Regexp), + _unused: T.nilable(T::Hash[Symbol, T.untyped]), + block: T.nilable(Proc), ).returns(T::Hash[Symbol, T.untyped]) } - def self.find_versions(url:, regex: nil, **unused, &block) + def self.find_versions(url:, regex: nil, **_unused, &block) + regex ||= GithubRelease::DEFAULT_REGEX + match_data = { matches: {}, regex: regex } generated = generate_input_values(url) + return match_data if generated.blank? + + release = GitHub.get_latest_release(generated[:username], generated[:repository]) + GithubRelease.versions_from_content([release], regex, &block).each do |match_text| + match_data[:matches][match_text] = Version.new(match_text) + end - PageMatch.find_versions(url: generated[:url], regex: regex || DEFAULT_REGEX, **unused, &block) + match_data end end end diff --git a/Library/Homebrew/livecheck/strategy/github_release.rb b/Library/Homebrew/livecheck/strategy/github_release.rb new file mode 100644 index 00000000000000..77eb77fb785a9c --- /dev/null +++ b/Library/Homebrew/livecheck/strategy/github_release.rb @@ -0,0 +1,136 @@ +# typed: true +# frozen_string_literal: true + +module Homebrew + module Livecheck + module Strategy + # The {Github} 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 GithubRelease + extend T::Sig + + NICE_NAME = "GitHub - Releases" + + # 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 = 9 + + # The `Regexp` used to determine if the strategy applies to the URL. + URL_MATCH_REGEX = %r{ + ^https?://github\.com + /(?:downloads/)?(?[^/]+) # The GitHub username + /(?[^/]+) # The GitHub repository name + }ix.freeze + + # The default regex used to identify a version from a tag when a regex + # isn't provided. + DEFAULT_REGEX = /v?(\d+(?:\.\d+)+)/i.freeze + + # Keys in the JSON that could contain the version. + VERSION_KEYS = ["name", "tag_name"].freeze + + # Whether the strategy can be applied to the provided URL. + # + # @param url [String] the URL to match against + # @return [Boolean] + sig { params(url: String).returns(T::Boolean) } + def self.match?(url) + URL_MATCH_REGEX.match?(url) + end + + # Uses the regex to match text in the content or, if a block is + # provided, passes the page content to the block to handle matching. + # With either approach, an array of unique matches is returned. + # + # @param content [String] the page content to check + # @param regex [Regexp, nil] a regex used for matching versions in the + # content + # @return [Array] + sig { + params( + content: T::Array[T::Hash[String, T.untyped]], + regex: T.nilable(Regexp), + block: T.nilable(Proc), + ).returns(T::Array[String]) + } + def self.versions_from_content(content, regex, &block) + if block + block_return_value = regex.present? ? yield(content, regex) : yield(content) + return Strategy.handle_block_return(block_return_value) + end + + values = [] + content.each do |release| + next if release.blank? + + VERSION_KEYS.each do |key| + match = release[key].match(regex) + + next if match.blank? + + values.push(match.to_s) + break + end + end + + values.compact.uniq + 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] + sig { + params( + url: String, + regex: T.nilable(Regexp), + _unused: T.nilable(T::Hash[Symbol, T.untyped]), + block: T.nilable(Proc), + ).returns(T::Hash[Symbol, T.untyped]) + } + def self.find_versions(url:, regex: nil, **_unused, &block) + match_data = { matches: {}, regex: regex || GithubRelease::DEFAULT_REGEX } + match = url.sub(/\.git$/i, "").match(URL_MATCH_REGEX) + + return match_data if match.blank? + + releases = GitHub::API.open_rest("https://api.github.com/repos/#{match[:username]}/#{match[:repository]}/releases") + + GithubRelease.versions_from_content(releases, regex || DEFAULT_REGEX, &block).each do |match_text| + match_data[:matches][match_text] = Version.new(match_text) + end + + match_data + end + end + end + end +end