mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00

The `eol_data` method uses `@eol_data["#{product}/#{cycle}"] ||=`, which can unncessarily allow a duplicate API call if the same product/cycle combination was previously tried but returned a 404 (Not Found) response. In this scenario, the value would be `nil` but the existing logic doesn't check whether this is a missing key or a `nil` value. If the key is present, we shouldn't make the same request again. This updates the method to return the existing value if the key exists, which effectively prevents duplicate fetches. This new logic only modifies `@eol_data` if `curl` is successful, so it does allow the request to be made again if it failed before. That said, this shouldn't normally be an issue and this is mostly about refactoring the method to allow for nicer code organization. This approach reduces the `begin` block to only the `JSON.parse` call, which allows us to use `return unless result.status.success?` (this previously led to a RuboCop offense because it was called within a `begin` block).
221 lines
8.0 KiB
Ruby
221 lines
8.0 KiB
Ruby
# typed: strict
|
|
# frozen_string_literal: true
|
|
|
|
require "utils/curl"
|
|
require "utils/github/api"
|
|
|
|
# Auditing functions for rules common to both casks and formulae.
|
|
module SharedAudits
|
|
URL_TYPE_HOMEPAGE = "homepage URL"
|
|
|
|
sig { params(product: String, cycle: String).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
def self.eol_data(product, cycle)
|
|
@eol_data ||= T.let({}, T.nilable(T::Hash[String, T.untyped]))
|
|
key = "#{product}/#{cycle}"
|
|
return @eol_data[key] if @eol_data.key?(key)
|
|
|
|
result = Utils::Curl.curl_output(
|
|
"--location",
|
|
"https://endoflife.date/api/v1/products/#{product}/releases/#{cycle}",
|
|
)
|
|
return unless result.status.success?
|
|
|
|
@eol_data[key] = begin
|
|
JSON.parse(result.stdout)
|
|
rescue JSON::ParserError
|
|
nil
|
|
end
|
|
end
|
|
|
|
sig { params(user: String, repo: String).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
def self.github_repo_data(user, repo)
|
|
@github_repo_data ||= T.let({}, T.nilable(T::Hash[String, T.untyped]))
|
|
@github_repo_data["#{user}/#{repo}"] ||= GitHub.repository(user, repo)
|
|
|
|
@github_repo_data["#{user}/#{repo}"]
|
|
rescue GitHub::API::HTTPNotFoundError
|
|
nil
|
|
rescue GitHub::API::AuthenticationFailedError => e
|
|
raise unless e.message.match?(GitHub::API::GITHUB_IP_ALLOWLIST_ERROR)
|
|
end
|
|
|
|
sig { params(user: String, repo: String, tag: String).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
private_class_method def self.github_release_data(user, repo, tag)
|
|
id = "#{user}/#{repo}/#{tag}"
|
|
url = "#{GitHub::API_URL}/repos/#{user}/#{repo}/releases/tags/#{tag}"
|
|
@github_release_data ||= T.let({}, T.nilable(T::Hash[String, T.untyped]))
|
|
@github_release_data[id] ||= GitHub::API.open_rest(url)
|
|
|
|
@github_release_data[id]
|
|
rescue GitHub::API::HTTPNotFoundError
|
|
nil
|
|
rescue GitHub::API::AuthenticationFailedError => e
|
|
raise unless e.message.match?(GitHub::API::GITHUB_IP_ALLOWLIST_ERROR)
|
|
end
|
|
|
|
sig {
|
|
params(
|
|
user: String, repo: String, tag: String, formula: T.nilable(Formula), cask: T.nilable(Cask::Cask),
|
|
).returns(
|
|
T.nilable(String),
|
|
)
|
|
}
|
|
def self.github_release(user, repo, tag, formula: nil, cask: nil)
|
|
release = github_release_data(user, repo, tag)
|
|
return unless release
|
|
|
|
exception, name, version = if formula
|
|
[formula.tap&.audit_exception(:github_prerelease_allowlist, formula.name), formula.name, formula.version]
|
|
elsif cask
|
|
[cask.tap&.audit_exception(:github_prerelease_allowlist, cask.token), cask.token, cask.version]
|
|
end
|
|
|
|
return "#{tag} is a GitHub pre-release." if release["prerelease"] && [version, "all", "any"].exclude?(exception)
|
|
|
|
if !release["prerelease"] && exception && [version, "any"].exclude?(exception)
|
|
return "#{tag} is not a GitHub pre-release but '#{name}' is in the GitHub prerelease allowlist."
|
|
end
|
|
|
|
"#{tag} is a GitHub draft." if release["draft"]
|
|
end
|
|
|
|
sig { params(user: String, repo: String).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
def self.gitlab_repo_data(user, repo)
|
|
@gitlab_repo_data ||= T.let({}, T.nilable(T::Hash[String, T.untyped]))
|
|
@gitlab_repo_data["#{user}/#{repo}"] ||= begin
|
|
result = Utils::Curl.curl_output("https://gitlab.com/api/v4/projects/#{user}%2F#{repo}")
|
|
json = JSON.parse(result.stdout) if result.status.success?
|
|
json = nil if json&.dig("message")&.include?("404 Project Not Found")
|
|
json
|
|
end
|
|
end
|
|
|
|
sig { params(user: String, repo: String, tag: String).returns(T.nilable(T::Hash[String, T.untyped])) }
|
|
private_class_method def self.gitlab_release_data(user, repo, tag)
|
|
id = "#{user}/#{repo}/#{tag}"
|
|
@gitlab_release_data ||= T.let({}, T.nilable(T::Hash[String, T.untyped]))
|
|
@gitlab_release_data[id] ||= begin
|
|
result = Utils::Curl.curl_output(
|
|
"https://gitlab.com/api/v4/projects/#{user}%2F#{repo}/releases/#{tag}", "--fail"
|
|
)
|
|
JSON.parse(result.stdout) if result.status.success?
|
|
end
|
|
end
|
|
|
|
sig {
|
|
params(
|
|
user: String, repo: String, tag: String, formula: T.nilable(Formula), cask: T.nilable(Cask::Cask),
|
|
).returns(
|
|
T.nilable(String),
|
|
)
|
|
}
|
|
def self.gitlab_release(user, repo, tag, formula: nil, cask: nil)
|
|
release = gitlab_release_data(user, repo, tag)
|
|
return unless release
|
|
|
|
return if DateTime.parse(release["released_at"]) <= DateTime.now
|
|
|
|
exception, version = if formula
|
|
[formula.tap&.audit_exception(:gitlab_prerelease_allowlist, formula.name), formula.version]
|
|
elsif cask
|
|
[cask.tap&.audit_exception(:gitlab_prerelease_allowlist, cask.token), cask.version]
|
|
end
|
|
return if [version, "all"].include?(exception)
|
|
|
|
"#{tag} is a GitLab pre-release."
|
|
end
|
|
|
|
sig { params(user: String, repo: String).returns(T.nilable(String)) }
|
|
def self.github(user, repo)
|
|
metadata = github_repo_data(user, repo)
|
|
|
|
return if metadata.nil?
|
|
|
|
return "GitHub fork (not canonical repository)" if metadata["fork"]
|
|
|
|
if (metadata["forks_count"] < 30) && (metadata["subscribers_count"] < 30) &&
|
|
(metadata["stargazers_count"] < 75)
|
|
return "GitHub repository not notable enough (<30 forks, <30 watchers and <75 stars)"
|
|
end
|
|
|
|
return if Date.parse(metadata["created_at"]) <= (Date.today - 30)
|
|
|
|
"GitHub repository too new (<30 days old)"
|
|
end
|
|
|
|
sig { params(user: String, repo: String).returns(T.nilable(String)) }
|
|
def self.gitlab(user, repo)
|
|
metadata = gitlab_repo_data(user, repo)
|
|
|
|
return if metadata.nil?
|
|
|
|
return "GitLab fork (not canonical repository)" if metadata["fork"]
|
|
if (metadata["forks_count"] < 30) && (metadata["star_count"] < 75)
|
|
return "GitLab repository not notable enough (<30 forks and <75 stars)"
|
|
end
|
|
|
|
return if Date.parse(metadata["created_at"]) <= (Date.today - 30)
|
|
|
|
"GitLab repository too new (<30 days old)"
|
|
end
|
|
|
|
sig { params(user: String, repo: String).returns(T.nilable(String)) }
|
|
def self.bitbucket(user, repo)
|
|
api_url = "https://api.bitbucket.org/2.0/repositories/#{user}/#{repo}"
|
|
result = Utils::Curl.curl_output("--request", "GET", api_url)
|
|
return unless result.status.success?
|
|
|
|
metadata = JSON.parse(result.stdout)
|
|
return if metadata.nil?
|
|
|
|
return "Uses deprecated Mercurial support in Bitbucket" if metadata["scm"] == "hg"
|
|
|
|
return "Bitbucket fork (not canonical repository)" unless metadata["parent"].nil?
|
|
|
|
return "Bitbucket repository too new (<30 days old)" if Date.parse(metadata["created_on"]) >= (Date.today - 30)
|
|
|
|
forks_result = Utils::Curl.curl_output("--request", "GET", "#{api_url}/forks")
|
|
return unless forks_result.status.success?
|
|
|
|
watcher_result = Utils::Curl.curl_output("--request", "GET", "#{api_url}/watchers")
|
|
return unless watcher_result.status.success?
|
|
|
|
forks_metadata = JSON.parse(forks_result.stdout)
|
|
return if forks_metadata.nil?
|
|
|
|
watcher_metadata = JSON.parse(watcher_result.stdout)
|
|
return if watcher_metadata.nil?
|
|
|
|
return if forks_metadata["size"] >= 30 || watcher_metadata["size"] >= 75
|
|
|
|
"Bitbucket repository not notable enough (<30 forks and <75 watchers)"
|
|
end
|
|
|
|
sig { params(url: String).returns(T.nilable(String)) }
|
|
def self.github_tag_from_url(url)
|
|
tag = url[%r{^https://github\.com/[\w-]+/[\w.-]+/archive/refs/tags/(.+)\.(tar\.gz|zip)$}, 1]
|
|
tag || url[%r{^https://github\.com/[\w-]+/[\w.-]+/releases/download/([^/]+)/}, 1]
|
|
end
|
|
|
|
sig { params(url: String).returns(T.nilable(String)) }
|
|
def self.gitlab_tag_from_url(url)
|
|
url[%r{^https://gitlab\.com/(?:\w[\w.-]*/){2,}-/archive/([^/]+)/}, 1]
|
|
end
|
|
|
|
sig { params(formula_or_cask: T.any(Formula, Cask::Cask)).returns(T.nilable(String)) }
|
|
def self.check_deprecate_disable_reason(formula_or_cask)
|
|
return if !formula_or_cask.deprecated? && !formula_or_cask.disabled?
|
|
|
|
reason = formula_or_cask.deprecated? ? formula_or_cask.deprecation_reason : formula_or_cask.disable_reason
|
|
return unless reason.is_a?(Symbol)
|
|
|
|
reasons = if formula_or_cask.is_a?(Formula)
|
|
DeprecateDisable::FORMULA_DEPRECATE_DISABLE_REASONS
|
|
else
|
|
DeprecateDisable::CASK_DEPRECATE_DISABLE_REASONS
|
|
end
|
|
|
|
"#{reason} is not a valid deprecate! or disable! reason" unless reasons.include?(reason)
|
|
end
|
|
end
|