Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use API for GitHub latest release strategy #15270

Merged
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
105 changes: 78 additions & 27 deletions Library/Homebrew/livecheck/strategy/github_latest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,25 @@ 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.com by checking a repository's "latest" release using the
# GitHub API.
#
# 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.
# {GithubLatest} should only be used when the upstream repository has a
# "latest" release for a suitable version and the strategy is necessary
# or appropriate (e.g. {Git} returns an unreleased version or the
# `stable` URL is a release asset). The strategy can only be applied by
# using `strategy :github_latest` in a `livecheck` block.
#
# 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.).
# The default regex identifies versions like `1.2.3`/`v1.2.3` in the
# release's tag name. 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. `1.2.3d`, `1.2.3-4`, etc.).
#
# @api public
class GithubLatest
Expand All @@ -48,7 +43,11 @@ class GithubLatest

# 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
DEFAULT_REGEX = /v?(\d+(?:\.\d+)+)/i.freeze

# Keys in the release JSON that could contain the version.
# Tag name first since that is closer to other livechecks.
VERSION_KEYS = ["tag_name", "name"].freeze

# Whether the strategy can be applied to the provided URL.
#
Expand All @@ -73,32 +72,84 @@ def self.generate_input_values(url)
match = url.sub(/\.git$/i, "").match(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[:url] = "https://api.github.com/repos/#{match[:username]}/#{match[:repository]}/releases/latest"
values[:username] = match[:username]
values[:repository] = match[:repository]

values
end

# Generates a URL and regex (if one isn't provided) and passes them
# to {PageMatch.find_versions} to identify versions in the content.
# Uses a regex to match the version from release JSON or, if a block is
# provided, passes the JSON to the block to handle matching. With
# either approach, an array of unique matches is returned.
#
# @param content [Array, Hash] list of releases or a single release
# @param regex [Regexp] a regex used for matching versions in the content
# @param block [Proc, nil] a block to match the content
# @return [Array]
sig {
params(
content: T.any(T::Array[T::Hash[String, T.untyped]], T::Hash[String, T.untyped]),
regex: Regexp,
block: T.nilable(Proc),
).returns(T::Array[String])
}
def self.versions_from_content(content, regex, &block)
if block.present?
block_return_value = if regex.present?
yield(content, regex)
else
yield(content)
end
return Strategy.handle_block_return(block_return_value)
end

content = [content] unless content.is_a?(Array)
content.reject(&:blank?).map do |release|
next if release["draft"] || release["prerelease"]

value = T.let(nil, T.untyped)
VERSION_KEYS.find do |key|
match = release[key]&.match(regex)
next if match.blank?

value = match[1]
end
value
end.compact.uniq
end

# Generates the GitHub API URL for the repository's "latest" release
# and identifies the version from the JSON response.
#
# @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),
url: String,
regex: 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: DEFAULT_REGEX, **_unused, &block)
match_data = { matches: {}, regex: regex, url: url }

generated = generate_input_values(url)
return match_data if generated.blank?

match_data[:url] = generated[:url]

release = GitHub.get_latest_release(generated[:username], generated[:repository])
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
GitHubLatest = Homebrew::Livecheck::Strategy::GithubLatest
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

let(:generated) do
{
url: "https://github.com/abc/def/releases/latest",
url: "https://api.github.com/repos/abc/def/releases/latest",
username: "abc",
repository: "def",
}
end

Expand Down