diff --git a/Library/Homebrew/cask/audit.rb b/Library/Homebrew/cask/audit.rb index dcb51d13ce..15ce7db195 100644 --- a/Library/Homebrew/cask/audit.rb +++ b/Library/Homebrew/cask/audit.rb @@ -928,6 +928,20 @@ module Cask add_error error, location: url.location if error end + sig { void } + def audit_forgejo_prerelease_version + return if (url = cask.url).nil? + + odebug "Auditing Forgejo prerelease" + user, repo = get_repo_data(%r{https?://codeberg\.org/([^/]+)/([^/]+)/?.*}) if online? + return if user.nil? || repo.nil? + + tag = SharedAudits.forgejo_tag_from_url(url.to_s) + tag ||= cask.version + error = SharedAudits.forgejo_release(user, repo, tag, cask:) + add_error error, location: url.location if error + end + sig { void } def audit_github_repository_archived # Deprecated/disabled casks may have an archived repository. @@ -960,6 +974,23 @@ module Cask add_error "GitLab repo is archived", location: url.location if metadata["archived"] end + sig { void } + def audit_forgejo_repository_archived + return if cask.deprecated? || cask.disabled? + return if (url = cask.url).nil? + + user, repo = get_repo_data(%r{https?://codeberg\.org/([^/]+)/([^/]+)/?.*}) if online? + return if user.nil? || repo.nil? + + metadata = SharedAudits.forgejo_repo_data(user, repo) + return if metadata.nil? + + return unless metadata["archived"] + + add_error "Forgejo repository is archived since #{metadata["archived_at"]}", + location: url.location + end + sig { void } def audit_github_repository return unless new_cask? @@ -1002,6 +1033,20 @@ module Cask add_error error, location: url.location if error end + sig { void } + def audit_forgejo_repository + return unless new_cask? + return if (url = cask.url).nil? + + user, repo = get_repo_data(%r{https?://codeberg\.org/([^/]+)/([^/]+)/?.*}) + return if user.nil? || repo.nil? + + odebug "Auditing Forgejo repo" + + error = SharedAudits.forgejo(user, repo) + add_error error, location: url.location if error + end + sig { void } def audit_denylist return unless cask.tap diff --git a/Library/Homebrew/formula_auditor.rb b/Library/Homebrew/formula_auditor.rb index 3e39551128..cec1bfbbb5 100644 --- a/Library/Homebrew/formula_auditor.rb +++ b/Library/Homebrew/formula_auditor.rb @@ -677,6 +677,19 @@ module Homebrew problem "GitLab repository is archived" if metadata["archived"] end + sig { void } + def audit_forgejo_repository_archived + return if formula.deprecated? || formula.disabled? + + user, repo = get_repo_data(%r{https?://codeberg\.org/([^/]+)/([^/]+)/?.*}) if @online + return if user.blank? + + metadata = SharedAudits.forgejo_repo_data(user, repo) + return if metadata.nil? + + problem "Forgejo repository is archived since #{metadata["archived_at"]}" if metadata["archived"] + end + def audit_github_repository user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) if @new_formula @@ -708,6 +721,17 @@ module Homebrew new_formula_problem warning end + sig { void } + def audit_forgejo_repository + user, repo = get_repo_data(%r{https?://codeberg\.org/([^/]+)/([^/]+)/?.*}) if @new_formula + return if user.blank? + + warning = SharedAudits.forgejo(user, repo) + return if warning.nil? + + new_formula_problem warning + end + def get_repo_data(regex) return unless @core_tap return unless @online @@ -839,6 +863,16 @@ module Homebrew error = SharedAudits.github_release(owner, repo, tag, formula:) problem error if error end + when %r{^https://codeberg\.org/([\w-]+)/([\w-]+)} + owner = T.must(Regexp.last_match(1)) + repo = T.must(Regexp.last_match(2)) + tag = SharedAudits.forgejo_tag_from_url(url) + tag ||= formula.stable.specs[:tag] + + if @online && !tag.nil? + error = SharedAudits.forgejo_release(owner, repo, tag, formula:) + problem error if error + end end end diff --git a/Library/Homebrew/test/utils/shared_audits_spec.rb b/Library/Homebrew/test/utils/shared_audits_spec.rb index 9d95b84ea9..312adcb4d5 100644 --- a/Library/Homebrew/test/utils/shared_audits_spec.rb +++ b/Library/Homebrew/test/utils/shared_audits_spec.rb @@ -96,4 +96,21 @@ RSpec.describe SharedAudits do expect(described_class.gitlab_tag_from_url(url)).to eq("2.5") end end + + describe "::forgejo_tag_from_url" do + it "finds tags in basic urls" do + url = "https://codeberg.org/Aviac/codeberg-cli/archive/v0.4.11.tar.gz" + expect(described_class.forgejo_tag_from_url(url)).to eq("v0.4.11") + end + + it "finds tags in urls with subgroups" do + url = "https://codeberg.org/Aviac/codeberg-cli/archive/some/test/1.2.3.tar.gz" + expect(described_class.forgejo_tag_from_url(url)).to eq("some/test/1.2.3") + end + + it "finds tags in orgs/repos with special characters" do + url = "https://codeberg.org/Aviaca-b_cv/codeberg-cli/archive/v0.4.11.tar.gz" + expect(described_class.forgejo_tag_from_url(url)).to eq("v0.4.11") + end + end end diff --git a/Library/Homebrew/utils/shared_audits.rb b/Library/Homebrew/utils/shared_audits.rb index 58b870e994..1fdc1e1921 100644 --- a/Library/Homebrew/utils/shared_audits.rb +++ b/Library/Homebrew/utils/shared_audits.rb @@ -90,6 +90,16 @@ module SharedAudits end end + sig { params(user: String, repo: String).returns(T.nilable(T::Hash[String, T.untyped])) } + def self.forgejo_repo_data(user, repo) + @forgejo_repo_data ||= T.let({}, T.nilable(T::Hash[String, T.untyped])) + @forgejo_repo_data["#{user}/#{repo}"] ||= begin + result = Utils::Curl.curl_output("https://codeberg.org/api/v1/repos/#{user}/#{repo}") + + JSON.parse(result.stdout) if result.status.success? + 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}" @@ -125,6 +135,40 @@ module SharedAudits "#{tag} is a GitLab pre-release." end + sig { params(user: String, repo: String, tag: String).returns(T.nilable(T::Hash[String, T.untyped])) } + private_class_method def self.forgejo_release_data(user, repo, tag) + id = "#{user}/#{repo}/#{tag}" + @forgejo_release_data ||= T.let({}, T.nilable(T::Hash[String, T.untyped])) + @forgejo_release_data[id] ||= begin + result = Utils::Curl.curl_output( + "https://codeberg.org/api/v1/repos/#{user}/#{repo}/releases/tags/#{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.forgejo_release(user, repo, tag, formula: nil, cask: nil) + release = forgejo_release_data(user, repo, tag) + return unless release + return unless release["prerelease"] + + exception, version = if formula + [formula.tap&.audit_exception(:forgejo_prerelease_allowlist, formula.name), formula.version] + elsif cask + [cask.tap&.audit_exception(:forgejo_prerelease_allowlist, cask.token), cask.version] + end + return if [version, "all"].include?(exception) + + "#{tag} is a Forgejo pre-release." + end + sig { params(user: String, repo: String).returns(T.nilable(String)) } def self.github(user, repo) metadata = github_repo_data(user, repo) @@ -191,6 +235,23 @@ module SharedAudits "Bitbucket repository not notable enough (<30 forks and <75 watchers)" end + sig { params(user: String, repo: String).returns(T.nilable(String)) } + def self.forgejo(user, repo) + metadata = forgejo_repo_data(user, repo) + return if metadata.nil? + + return "Forgejo fork (not canonical repository)" if metadata["fork"] + + if (metadata["forks_count"] < 30) && (metadata["watchers_count"] < 30) && + (metadata["stars_count"] < 75) + return "Forgejo repository not notable enough (<30 forks, <30 watchers and <75 stars)" + end + + return if Date.parse(metadata["created_at"]) <= (Date.today - 30) + + "Forgejo repository too new (<30 days old)" + 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] @@ -202,6 +263,11 @@ module SharedAudits url[%r{^https://gitlab\.com/(?:\w[\w.-]*/){2,}-/archive/([^/]+)/}, 1] end + sig { params(url: String).returns(T.nilable(String)) } + def self.forgejo_tag_from_url(url) + url[%r{^https://codeberg\.org/[\w-]+/[\w.-]+/archive/(.+)\.(tar\.gz|zip)$}, 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?