Add notibility checks for casks

This commit is contained in:
Sean Molenaar 2020-04-23 21:16:17 +02:00
parent 8164e0804f
commit 3567892802
No known key found for this signature in database
GPG Key ID: 6BF5D8DF0D34FAAE
7 changed files with 349 additions and 108 deletions

View File

@ -6,6 +6,7 @@ require "cask/download"
require "digest" require "digest"
require "utils/curl" require "utils/curl"
require "utils/git" require "utils/git"
require "utils/notability"
module Cask module Cask
class Audit class Audit
@ -14,22 +15,22 @@ module Cask
attr_reader :cask, :commit_range, :download attr_reader :cask, :commit_range, :download
attr_predicate :check_appcast? attr_predicate :appcast?
def initialize(cask, check_appcast: false, download: false, check_token_conflicts: false, def initialize(cask, appcast: false, download: false,
commit_range: nil, command: SystemCommand) token_conflicts: false, online: false, strict: false,
new_cask: false, commit_range: nil, command: SystemCommand)
@cask = cask @cask = cask
@check_appcast = check_appcast @appcast = appcast
@download = download @download = download
@online = online
@strict = strict
@new_cask = new_cask
@commit_range = commit_range @commit_range = commit_range
@check_token_conflicts = check_token_conflicts @token_conflicts = token_conflicts
@command = command @command = command
end end
def check_token_conflicts?
@check_token_conflicts
end
def run! def run!
check_blacklist check_blacklist
check_required_stanzas check_required_stanzas
@ -48,6 +49,9 @@ module Cask
check_latest_with_auto_updates check_latest_with_auto_updates
check_stanza_requires_uninstall check_stanza_requires_uninstall
check_appcast_contains_version check_appcast_contains_version
check_github_repository
check_gitlab_repository
check_bitbucket_repository
self self
rescue => e rescue => e
odebug "#{e.message}\n#{e.backtrace.join("\n")}" odebug "#{e.message}\n#{e.backtrace.join("\n")}"
@ -272,7 +276,7 @@ module Cask
end end
def check_token_conflicts def check_token_conflicts
return unless check_token_conflicts? return unless @token_conflicts
return unless core_formula_names.include?(cask.token) return unless core_formula_names.include?(cask.token)
add_warning "possible duplicate, cask token conflicts with Homebrew core formula: #{core_formula_url}" add_warning "possible duplicate, cask token conflicts with Homebrew core formula: #{core_formula_url}"
@ -301,7 +305,7 @@ module Cask
end end
def check_appcast_contains_version def check_appcast_contains_version
return unless check_appcast? return unless appcast?
return if cask.appcast.to_s.empty? return if cask.appcast.to_s.empty?
return if cask.appcast.configuration == :no_check return if cask.appcast.configuration == :no_check
@ -322,6 +326,50 @@ module Cask
add_error "appcast at URL '#{appcast_stanza}' offline or looping" add_error "appcast at URL '#{appcast_stanza}' offline or looping"
end end
def check_github_repository
user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*})
return if user.nil?
odebug "Auditing GitHub repo"
error = SharedAudits.github(user, repo)
add_error error if error
end
def check_gitlab_repository
user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*})
return if user.nil?
odebug "Auditing GitLab repo"
error = SharedAudits.gitlab(user, repo)
add_error error if error
end
def check_bitbucket_repository
user, repo = get_repo_data(%r{https?://bitbucket\.org/([^/]+)/([^/]+)/?.*})
return if user.nil?
odebug "Auditing Bitbucket repo"
error = SharedAudits.bitbucket(user, repo)
add_error error if error
end
def get_repo_data(regex)
return unless @online
return unless @new_cask
_, user, repo = *regex.match(cask.url.to_s)
_, user, repo = *regex.match(cask.homepage) unless user
_, user, repo = *regex.match(cask.appcast.to_s) unless user
return if !user || !repo
repo.gsub!(/.git$/, "")
[user, repo]
end
def check_blacklist def check_blacklist
return if cask.tap&.user != "Homebrew" return if cask.tap&.user != "Homebrew"
return unless reason = Blacklist.blacklisted_reason(cask.token) return unless reason = Blacklist.blacklisted_reason(cask.token)

View File

@ -8,34 +8,37 @@ module Cask
extend Predicable extend Predicable
def self.audit(cask, audit_download: false, audit_appcast: false, def self.audit(cask, audit_download: false, audit_appcast: false,
check_token_conflicts: false, quarantine: true, commit_range: nil) audit_online: false, audit_strict: false,
audit_token_conflicts: false, audit_new_cask: false,
quarantine: true, commit_range: nil)
new(cask, audit_download: audit_download, new(cask, audit_download: audit_download,
audit_appcast: audit_appcast, audit_appcast: audit_appcast,
check_token_conflicts: check_token_conflicts, audit_online: audit_online,
audit_new_cask: audit_new_cask,
audit_strict: audit_strict,
audit_token_conflicts: audit_token_conflicts,
quarantine: quarantine, commit_range: commit_range).audit quarantine: quarantine, commit_range: commit_range).audit
end end
attr_reader :cask, :commit_range attr_reader :cask, :commit_range
def initialize(cask, audit_download: false, audit_appcast: false, def initialize(cask, audit_download: false, audit_appcast: false,
check_token_conflicts: false, quarantine: true, commit_range: nil) audit_online: false, audit_strict: false,
audit_token_conflicts: false, audit_new_cask: false,
quarantine: true, commit_range: nil)
@cask = cask @cask = cask
@audit_download = audit_download @audit_download = audit_download
@audit_appcast = audit_appcast @audit_appcast = audit_appcast
@audit_online = audit_online
@audit_strict = audit_strict
@audit_new_cask = audit_new_cask
@quarantine = quarantine @quarantine = quarantine
@commit_range = commit_range @commit_range = commit_range
@check_token_conflicts = check_token_conflicts @audit_token_conflicts = audit_token_conflicts
end end
def audit_download? attr_predicate :audit_appcast?, :audit_download?, :audit_online?,
@audit_download :audit_strict?, :audit_new_cask?, :audit_token_conflicts?, :quarantine?
end
attr_predicate :audit_appcast?, :quarantine?
def check_token_conflicts?
@check_token_conflicts
end
def audit def audit
if !Homebrew.args.value("language") && language_blocks if !Homebrew.args.value("language") && language_blocks
@ -64,10 +67,13 @@ module Cask
def audit_cask_instance(cask) def audit_cask_instance(cask)
download = audit_download? && Download.new(cask, quarantine: quarantine?) download = audit_download? && Download.new(cask, quarantine: quarantine?)
audit = Audit.new(cask, check_appcast: audit_appcast?, audit = Audit.new(cask, appcast: audit_appcast?,
download: download, online: audit_online?,
check_token_conflicts: check_token_conflicts?, strict: audit_strict?,
commit_range: commit_range) new_cask: audit_new_cask?,
token_conflicts: audit_token_conflicts?,
download: download,
commit_range: commit_range)
audit.run! audit.run!
puts audit.summary puts audit.summary
audit.success? audit.success?

View File

@ -1,32 +1,66 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cli/parser"
module Cask module Cask
class Cmd class Cmd
class Audit < AbstractCommand class Audit < AbstractCommand
option "--download", :download, false option "--download", :download_arg, false
option "--appcast", :appcast, false option "--appcast", :appcast_arg, false
option "--token-conflicts", :token_conflicts, false option "--token-conflicts", :token_conflicts_arg, false
option "--strict", :strict_arg, false
option "--online", :online_arg, false
option "--new-cask", :new_cask_arg, false
def self.usage
<<~EOS
`cask audit` [<options>] [<cask>]
--strict - Run additional, stricter style checks.
--online - Run additional, slower style checks that require a network connection.
--new-cask - Run various additional style checks to determine if a new cask is eligible
for Homebrew. This should be used when creating new casks and implies
`--strict` and `--online`.
--download - Audit the downloaded file
--appcast - Audit the appcast
--token-conflicts - Audit for token conflicts
Check <cask> for Homebrew coding style violations. This should be run before
submitting a new cask. If no <casks> are provided, check all locally
available casks. Will exit with a non-zero status if any errors are
found, which can be useful for implementing pre-commit hooks.
EOS
end
def self.help def self.help
"verifies installability of Casks" "verifies installability of Casks"
end end
def run def run
Homebrew.auditing = true
strict = new_cask_arg? || strict_arg?
token_conflicts = strict || token_conflicts_arg?
online = new_cask_arg? || online_arg?
download = online || download_arg?
appcast = online || appcast_arg?
failed_casks = casks(alternative: -> { Cask.to_a }) failed_casks = casks(alternative: -> { Cask.to_a })
.reject { |cask| audit(cask) } .reject do |cask|
odebug "Auditing Cask #{cask}"
Auditor.audit(cask, audit_download: download,
audit_appcast: appcast,
audit_online: online,
audit_strict: strict,
audit_new_cask: new_cask_arg?,
audit_token_conflicts: token_conflicts,
quarantine: quarantine?)
end
return if failed_casks.empty? return if failed_casks.empty?
raise CaskError, "audit failed for casks: #{failed_casks.join(" ")}" raise CaskError, "audit failed for casks: #{failed_casks.join(" ")}"
end end
def audit(cask)
odebug "Auditing Cask #{cask}"
Auditor.audit(cask, audit_download: download?,
audit_appcast: appcast?,
check_token_conflicts: token_conflicts?,
quarantine: quarantine?)
end
end end
end end
end end

View File

@ -3,6 +3,7 @@
require "formula" require "formula"
require "formula_versions" require "formula_versions"
require "utils/curl" require "utils/curl"
require "utils/notability"
require "extend/ENV" require "extend/ENV"
require "formula_cellar_checks" require "formula_cellar_checks"
require "cmd/search" require "cmd/search"
@ -510,79 +511,30 @@ module Homebrew
user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*}) user, repo = get_repo_data(%r{https?://github\.com/([^/]+)/([^/]+)/?.*})
return if user.nil? return if user.nil?
begin warning = SharedAudits.github(user, repo)
metadata = GitHub.repository(user, repo) return if warning.nil?
rescue GitHub::HTTPNotFoundError
return
end
return if metadata.nil? new_formula_problem warning
new_formula_problem "GitHub fork (not canonical repository)" if metadata["fork"]
if (metadata["forks_count"] < 30) && (metadata["subscribers_count"] < 30) &&
(metadata["stargazers_count"] < 75)
new_formula_problem "GitHub repository not notable enough (<30 forks, <30 watchers and <75 stars)"
end
return if Date.parse(metadata["created_at"]) <= (Date.today - 30)
new_formula_problem "GitHub repository too new (<30 days old)"
end end
def audit_gitlab_repository def audit_gitlab_repository
user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*}) user, repo = get_repo_data(%r{https?://gitlab\.com/([^/]+)/([^/]+)/?.*})
return if user.nil? return if user.nil?
out, _, status= curl_output("--request", "GET", "https://gitlab.com/api/v4/projects/#{user}%2F#{repo}") warning = SharedAudits.gitlab(user, repo)
return unless status.success? return if warning.nil?
metadata = JSON.parse(out) new_formula_problem warning
return if metadata.nil?
new_formula_problem "GitLab fork (not canonical repository)" if metadata["fork"]
if (metadata["forks_count"] < 30) && (metadata["star_count"] < 75)
new_formula_problem "GitLab repository not notable enough (<30 forks and <75 stars)"
end
return if Date.parse(metadata["created_at"]) <= (Date.today - 30)
new_formula_problem "GitLab repository too new (<30 days old)"
end end
def audit_bitbucket_repository def audit_bitbucket_repository
user, repo = get_repo_data(%r{https?://bitbucket\.org/([^/]+)/([^/]+)/?.*}) user, repo = get_repo_data(%r{https?://bitbucket\.org/([^/]+)/([^/]+)/?.*})
return if user.nil? return if user.nil?
api_url = "https://api.bitbucket.org/2.0/repositories/#{user}/#{repo}" warning = SharedAudits.bitbucket(user, repo)
out, _, status= curl_output("--request", "GET", api_url) return if warning.nil?
return unless status.success?
metadata = JSON.parse(out) new_formula_problem warning
return if metadata.nil?
new_formula_problem "Uses deprecated mercurial support in Bitbucket" if metadata["scm"] == "hg"
new_formula_problem "Bitbucket fork (not canonical repository)" unless metadata["parent"].nil?
if Date.parse(metadata["created_on"]) >= (Date.today - 30)
new_formula_problem "Bitbucket repository too new (<30 days old)"
end
forks_out, _, forks_status= curl_output("--request", "GET", "#{api_url}/forks")
return unless forks_status.success?
watcher_out, _, watcher_status= curl_output("--request", "GET", "#{api_url}/watchers")
return unless watcher_status.success?
forks_metadata = JSON.parse(forks_out)
return if forks_metadata.nil?
watcher_metadata = JSON.parse(watcher_out)
return if watcher_metadata.nil?
return if (forks_metadata["size"] < 30) && (watcher_metadata["size"] < 75)
new_formula_problem "Bitbucket repository not notable enough (<30 forks and <75 watchers)"
end end
def get_repo_data(regex) def get_repo_data(regex)

View File

@ -39,12 +39,12 @@ describe Cask::Audit, :cask do
let(:cask) { instance_double(Cask::Cask) } let(:cask) { instance_double(Cask::Cask) }
let(:download) { false } let(:download) { false }
let(:check_token_conflicts) { false } let(:token_conflicts) { false }
let(:fake_system_command) { class_double(SystemCommand) } let(:fake_system_command) { class_double(SystemCommand) }
let(:audit) { let(:audit) {
described_class.new(cask, download: download, described_class.new(cask, download: download,
check_token_conflicts: check_token_conflicts, token_conflicts: token_conflicts,
command: fake_system_command) command: fake_system_command)
} }
describe "#result" do describe "#result" do
@ -517,7 +517,7 @@ describe Cask::Audit, :cask do
describe "token conflicts" do describe "token conflicts" do
let(:cask_token) { "with-binary" } let(:cask_token) { "with-binary" }
let(:check_token_conflicts) { true } let(:token_conflicts) { true }
context "when cask token conflicts with a core formula" do context "when cask token conflicts with a core formula" do
let(:formula_names) { %w[with-binary other-formula] } let(:formula_names) { %w[with-binary other-formula] }

View File

@ -21,7 +21,13 @@ describe Cask::Cmd::Audit, :cask do
expect(Cask::CaskLoader).to receive(:load).with(cask_token).and_return(cask) expect(Cask::CaskLoader).to receive(:load).with(cask_token).and_return(cask)
expect(Cask::Auditor).to receive(:audit) expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: false, audit_appcast: false, check_token_conflicts: false, quarantine: true) .with(cask, audit_download: false,
audit_appcast: false,
audit_token_conflicts: false,
audit_new_cask: false,
audit_online: false,
audit_strict: false,
quarantine: true)
.and_return(true) .and_return(true)
described_class.run(cask_token) described_class.run(cask_token)
@ -32,7 +38,13 @@ describe Cask::Cmd::Audit, :cask do
it "does not download the Cask per default" do it "does not download the Cask per default" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask) allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit) expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: false, audit_appcast: false, check_token_conflicts: false, quarantine: true) .with(cask, audit_download: false,
audit_appcast: false,
audit_token_conflicts: false,
audit_new_cask: false,
audit_online: false,
audit_strict: false,
quarantine: true)
.and_return(true) .and_return(true)
described_class.run("casktoken") described_class.run("casktoken")
@ -41,7 +53,13 @@ describe Cask::Cmd::Audit, :cask do
it "download a Cask if --download flag is set" do it "download a Cask if --download flag is set" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask) allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit) expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: true, audit_appcast: false, check_token_conflicts: false, quarantine: true) .with(cask, audit_download: true,
audit_appcast: false,
audit_token_conflicts: false,
audit_new_cask: false,
audit_online: false,
audit_strict: false,
quarantine: true)
.and_return(true) .and_return(true)
described_class.run("casktoken", "--download") described_class.run("casktoken", "--download")
@ -52,7 +70,13 @@ describe Cask::Cmd::Audit, :cask do
it "does not check for token conflicts per default" do it "does not check for token conflicts per default" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask) allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit) expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: false, audit_appcast: false, check_token_conflicts: false, quarantine: true) .with(cask, audit_download: false,
audit_appcast: false,
audit_token_conflicts: false,
audit_new_cask: false,
audit_online: false,
audit_strict: false,
quarantine: true)
.and_return(true) .and_return(true)
described_class.run("casktoken") described_class.run("casktoken")
@ -61,10 +85,112 @@ describe Cask::Cmd::Audit, :cask do
it "checks for token conflicts if --token-conflicts flag is set" do it "checks for token conflicts if --token-conflicts flag is set" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask) allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit) expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: false, audit_appcast: false, check_token_conflicts: true, quarantine: true) .with(cask, audit_download: false,
audit_appcast: false,
audit_token_conflicts: true,
audit_new_cask: false,
audit_online: false,
audit_strict: false,
quarantine: true)
.and_return(true) .and_return(true)
described_class.run("casktoken", "--token-conflicts") described_class.run("casktoken", "--token-conflicts")
end end
end end
describe "rules for checking strictly" do
it "does not check strictly per default" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: false,
audit_appcast: false,
audit_token_conflicts: false,
audit_new_cask: false,
audit_online: false,
audit_strict: false,
quarantine: true)
.and_return(true)
described_class.run("casktoken")
end
it "checks strictly if --strict flag is set" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: false,
audit_appcast: false,
audit_token_conflicts: true,
audit_new_cask: false,
audit_online: false,
audit_strict: true,
quarantine: true)
.and_return(true)
described_class.run("casktoken", "--strict")
end
end
describe "rules for checking online" do
it "does not check online per default" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: false,
audit_appcast: false,
audit_token_conflicts: false,
audit_new_cask: false,
audit_online: false,
audit_strict: false,
quarantine: true)
.and_return(true)
described_class.run("casktoken")
end
it "checks online if --online flag is set" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: true,
audit_appcast: true,
audit_token_conflicts: false,
audit_new_cask: false,
audit_online: true,
audit_strict: false,
quarantine: true)
.and_return(true)
described_class.run("casktoken", "--online")
end
end
describe "rules for checking new casks" do
it "does not check new casks per default" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: false,
audit_appcast: false,
audit_token_conflicts: false,
audit_new_cask: false,
audit_online: false,
audit_strict: false,
quarantine: true)
.and_return(true)
described_class.run("casktoken")
end
it "checks new casks if --new-cask flag is set" do
allow(Cask::CaskLoader).to receive(:load).and_return(cask)
expect(Cask::Auditor).to receive(:audit)
.with(cask, audit_download: true,
audit_appcast: true,
audit_token_conflicts: true,
audit_new_cask: true,
audit_online: true,
audit_strict: true,
quarantine: true)
.and_return(true)
described_class.run("casktoken", "--new-cask")
end
end
end end

View File

@ -0,0 +1,75 @@
# frozen_string_literal: true
require "utils/curl"
module SharedAudits
module_function
def github(user, repo)
begin
metadata = GitHub.repository(user, repo)
rescue GitHub::HTTPNotFoundError
return
end
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
def gitlab(user, repo)
out, _, status= curl_output("--request", "GET", "https://gitlab.com/api/v4/projects/#{user}%2F#{repo}")
return unless status.success?
metadata = JSON.parse(out)
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
def bitbucket(user, repo)
api_url = "https://api.bitbucket.org/2.0/repositories/#{user}/#{repo}"
out, _, status= curl_output("--request", "GET", api_url)
return unless status.success?
metadata = JSON.parse(out)
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_out, _, forks_status= curl_output("--request", "GET", "#{api_url}/forks")
return unless forks_status.success?
watcher_out, _, watcher_status= curl_output("--request", "GET", "#{api_url}/watchers")
return unless watcher_status.success?
forks_metadata = JSON.parse(forks_out)
return if forks_metadata.nil?
watcher_metadata = JSON.parse(watcher_out)
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
end