Merge remote-tracking branch 'origin/master' into test-runners

This commit is contained in:
Carlo Cabrera 2023-04-07 22:26:05 +08:00
commit a754b6d343
No known key found for this signature in database
GPG Key ID: C74D447FC549A1D0
34 changed files with 469 additions and 242 deletions

View File

@ -64,4 +64,4 @@ jobs:
- name: Process rubydoc comments - name: Process rubydoc comments
working-directory: ${{ steps.set-up-homebrew.outputs.repository-path }}/Library/Homebrew working-directory: ${{ steps.set-up-homebrew.outputs.repository-path }}/Library/Homebrew
run: bundle exec yard doc --plugin sorbet --no-output --fail-on-warning run: bundle exec yard doc --no-output --fail-on-warning

View File

@ -151,7 +151,7 @@ jobs:
run: brew readall --aliases homebrew/core run: brew readall --aliases homebrew/core
- name: Run brew audit --skip-style on homebrew/core - name: Run brew audit --skip-style on homebrew/core
run: brew audit --skip-style --except=version --display-failures-only --tap=homebrew/core run: brew audit --skip-style --except=version --tap=homebrew/core
cask-audit: cask-audit:
name: cask audit name: cask audit
@ -187,10 +187,10 @@ jobs:
- name: Run brew audit --skip-style on casks - name: Run brew audit --skip-style on casks
run: | run: |
brew audit --skip-style --except=version --display-failures-only --tap=homebrew/cask brew audit --skip-style --except=version --tap=homebrew/cask
brew audit --skip-style --except=version --display-failures-only --tap=homebrew/cask-drivers brew audit --skip-style --except=version --tap=homebrew/cask-drivers
brew audit --skip-style --except=version --display-failures-only --tap=homebrew/cask-fonts brew audit --skip-style --except=version --tap=homebrew/cask-fonts
brew audit --skip-style --except=version --display-failures-only --tap=homebrew/cask-versions brew audit --skip-style --except=version --tap=homebrew/cask-versions
vendored-gems: vendored-gems:
name: vendored gems name: vendored gems

View File

@ -2,6 +2,7 @@
--main README.md --main README.md
--markup markdown --markup markdown
--no-private --no-private
--plugin sorbet
--load yard/ignore_directives.rb --load yard/ignore_directives.rb
--template-path yard/templates --template-path yard/templates
--exclude test/ --exclude test/

View File

@ -7,7 +7,7 @@ GEM
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
zeitwerk (~> 2.3) zeitwerk (~> 2.3)
addressable (2.8.2) addressable (2.8.3)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2) ast (2.4.2)
bindata (2.4.15) bindata (2.4.15)

View File

@ -71,53 +71,31 @@ module Cask
@errors ||= [] @errors ||= []
end end
def warnings
@warnings ||= []
end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def errors? def errors?
errors.any? errors.any?
end end
sig { returns(T::Boolean) }
def warnings?
warnings.any?
end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def success? def success?
!(errors? || warnings?) !errors?
end end
sig { params(message: T.nilable(String), location: T.nilable(String)).void } sig { params(message: T.nilable(String), location: T.nilable(String), strict_only: T::Boolean).void }
def add_error(message, location: nil) def add_error(message, location: nil, strict_only: false)
# Only raise non-critical audits if the user specified `--strict`.
return if strict_only && !@strict
errors << ({ message: message, location: location }) errors << ({ message: message, location: location })
end end
sig { params(message: T.nilable(String), location: T.nilable(String)).void }
def add_warning(message, location: nil)
if strict?
add_error message, location: location
else
warnings << ({ message: message, location: location })
end
end
def result def result
if errors? Formatter.error("failed") if errors?
Formatter.error("failed")
elsif warnings?
Formatter.warning("warning")
else
Formatter.success("passed")
end
end end
sig { params(include_passed: T::Boolean, include_warnings: T::Boolean).returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def summary(include_passed: false, include_warnings: true) def summary
return if success? && !include_passed return if success?
return if warnings? && !errors? && !include_warnings
summary = ["audit for #{cask}: #{result}"] summary = ["audit for #{cask}: #{result}"]
@ -125,12 +103,6 @@ module Cask
summary << " #{Formatter.error("-")} #{error[:message]}" summary << " #{Formatter.error("-")} #{error[:message]}"
end end
if include_warnings
warnings.each do |warning|
summary << " #{Formatter.warning("-")} #{warning[:message]}"
end
end
summary.join("\n") summary.join("\n")
end end
@ -220,7 +192,7 @@ module Cask
# increases the maintenance burden. # increases the maintenance burden.
return if cask.tap == "homebrew/cask-fonts" return if cask.tap == "homebrew/cask-fonts"
add_warning "Cask should have a description. Please add a `desc` stanza." if cask.desc.blank? add_error("Cask should have a description. Please add a `desc` stanza.", strict_only: true) if cask.desc.blank?
end end
sig { void } sig { void }
@ -408,8 +380,10 @@ module Cask
return unless 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: " \ add_error(
"#{Formatter.url(core_formula_url)}" "possible duplicate, cask token conflicts with Homebrew core formula: #{Formatter.url(core_formula_url)}",
strict_only: true,
)
end end
sig { void } sig { void }
@ -443,18 +417,19 @@ module Cask
add_error "cask token contains version designation '#{match_data[:designation]}'" add_error "cask token contains version designation '#{match_data[:designation]}'"
end end
add_warning "cask token mentions launcher" if token.end_with? "launcher" add_error("cask token mentions launcher", strict_only: true) if token.end_with? "launcher"
add_warning "cask token mentions desktop" if token.end_with? "desktop" add_error("cask token mentions desktop", strict_only: true) if token.end_with? "desktop"
add_warning "cask token mentions platform" if token.end_with? "mac", "osx", "macos" add_error("cask token mentions platform", strict_only: true) if token.end_with? "mac", "osx", "macos"
add_warning "cask token mentions architecture" if token.end_with? "x86", "32_bit", "x86_64", "64_bit" add_error("cask token mentions architecture", strict_only: true) if token.end_with? "x86", "32_bit", "x86_64",
"64_bit"
frameworks = %w[cocoa qt gtk wx java] frameworks = %w[cocoa qt gtk wx java]
return if frameworks.include?(token) || !token.end_with?(*frameworks) return if frameworks.include?(token) || !token.end_with?(*frameworks)
add_warning "cask token mentions framework" add_error("cask token mentions framework", strict_only: true)
end end
sig { void } sig { void }
@ -474,7 +449,10 @@ module Cask
return if cask.url.to_s.include? cask.version.csv.second return if cask.url.to_s.include? cask.version.csv.second
return if cask.version.csv.third.present? && cask.url.to_s.include?(cask.version.csv.third) return if cask.version.csv.third.present? && cask.url.to_s.include?(cask.version.csv.third)
add_warning "Download does not require additional version components. Use `&:short_version` in the livecheck" add_error(
"Download does not require additional version components. Use `&:short_version` in the livecheck",
strict_only: true,
)
end end
sig { void } sig { void }
@ -518,7 +496,7 @@ module Cask
"#{message} fix the signature of their app." "#{message} fix the signature of their app."
end end
add_warning message add_error(message, strict_only: true)
when Artifact::Pkg when Artifact::Pkg
path = downloaded_path path = downloaded_path
next unless path.exist? next unless path.exist?
@ -526,7 +504,7 @@ module Cask
result = system_command("pkgutil", args: ["--check-signature", path], print_stderr: false) result = system_command("pkgutil", args: ["--check-signature", path], print_stderr: false)
unless result.success? unless result.success?
add_warning <<~EOS add_error(<<~EOS, strict_only: true)
Signature verification failed: Signature verification failed:
#{result.merged_output} #{result.merged_output}
macOS on ARM requires applications to be signed. macOS on ARM requires applications to be signed.
@ -538,7 +516,7 @@ module Cask
result = system_command("stapler", args: ["validate", path], print_stderr: false) result = system_command("stapler", args: ["validate", path], print_stderr: false)
next if result.success? next if result.success?
add_warning <<~EOS add_error(<<~EOS, strict_only: true)
Signature verification failed: Signature verification failed:
#{result.merged_output} #{result.merged_output}
macOS on ARM requires applications to be signed. macOS on ARM requires applications to be signed.
@ -663,16 +641,10 @@ module Cask
metadata = SharedAudits.github_repo_data(user, repo) metadata = SharedAudits.github_repo_data(user, repo)
return if metadata.nil? return if metadata.nil?
return unless metadata["archived"] return unless metadata["archived"]
message = "GitHub repo is archived" # Discontinued casks shouldn't show up as errors.
add_error("GitHub repo is archived", strict_only: !cask.discontinued?)
if cask.discontinued?
add_warning message
else
add_error message
end
end end
sig { void } sig { void }
@ -684,16 +656,10 @@ module Cask
metadata = SharedAudits.gitlab_repo_data(user, repo) metadata = SharedAudits.gitlab_repo_data(user, repo)
return if metadata.nil? return if metadata.nil?
return unless metadata["archived"] return unless metadata["archived"]
message = "GitLab repo is archived" # Discontinued casks shouldn't show up as errors.
add_error("GitLab repo is archived") unless cask.discontinued?
if cask.discontinued?
add_warning message
else
add_error message
end
end end
sig { void } sig { void }

View File

@ -25,8 +25,6 @@ module Cask
quarantine: nil, quarantine: nil,
any_named_args: nil, any_named_args: nil,
language: nil, language: nil,
display_passes: nil,
display_failures_only: nil,
only: [], only: [],
except: [] except: []
) )
@ -40,8 +38,6 @@ module Cask
@audit_token_conflicts = audit_token_conflicts @audit_token_conflicts = audit_token_conflicts
@any_named_args = any_named_args @any_named_args = any_named_args
@language = language @language = language
@display_passes = display_passes
@display_failures_only = display_failures_only
@only = only @only = only
@except = except @except = except
end end
@ -49,7 +45,6 @@ module Cask
LANGUAGE_BLOCK_LIMIT = 10 LANGUAGE_BLOCK_LIMIT = 10
def audit def audit
warnings = Set.new
errors = Set.new errors = Set.new
if !language && language_blocks if !language && language_blocks
@ -63,23 +58,19 @@ module Cask
sample_languages.each_key do |l| sample_languages.each_key do |l|
audit = audit_languages(l) audit = audit_languages(l)
summary = audit.summary(include_passed: output_passed?, include_warnings: output_warnings?) if audit.summary.present? && output_summary?(audit)
if summary.present? && output_summary?(audit)
ohai "Auditing language: #{l.map { |lang| "'#{lang}'" }.to_sentence}" if output_summary? ohai "Auditing language: #{l.map { |lang| "'#{lang}'" }.to_sentence}" if output_summary?
puts summary puts audit.summary
end end
warnings += audit.warnings
errors += audit.errors errors += audit.errors
end end
else else
audit = audit_cask_instance(cask) audit = audit_cask_instance(cask)
summary = audit.summary(include_passed: output_passed?, include_warnings: output_warnings?) puts audit.summary if audit.summary.present? && output_summary?(audit)
puts summary if summary.present? && output_summary?(audit)
warnings += audit.warnings
errors += audit.errors errors += audit.errors
end end
{ warnings: warnings, errors: errors } errors
end end
private private
@ -92,19 +83,6 @@ module Cask
audit.errors? audit.errors?
end end
def output_passed?
return false if @display_failures_only.present?
return true if @display_passes.present?
false
end
def output_warnings?
return false if @display_failures_only.present?
true
end
def audit_languages(languages) def audit_languages(languages)
original_config = cask.config original_config = cask.config
localized_config = original_config.merge(Config.new(explicit: { languages: languages })) localized_config = original_config.merge(Config.new(explicit: { languages: languages }))

View File

@ -30,8 +30,6 @@ module Cask
description: "Run various additional style checks to determine if a new cask is eligible " \ description: "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 " \ "for Homebrew. This should be used when creating new casks and implies " \
"`--strict` and `--online`" "`--strict` and `--online`"
switch "--display-failures-only",
description: "Only display casks that fail the audit. This is the default for formulae."
end end
end end
@ -49,19 +47,17 @@ module Cask
results = self.class.audit_casks( results = self.class.audit_casks(
*casks, *casks,
download: args.download?, download: args.download?,
online: args.online?, online: args.online?,
strict: args.strict?, strict: args.strict?,
signing: args.signing?, signing: args.signing?,
new_cask: args.new_cask?, new_cask: args.new_cask?,
token_conflicts: args.token_conflicts?, token_conflicts: args.token_conflicts?,
quarantine: args.quarantine?, quarantine: args.quarantine?,
any_named_args: any_named_args, any_named_args: any_named_args,
language: args.language, language: args.language,
display_passes: args.verbose? || args.named.count == 1, only: [],
display_failures_only: args.display_failures_only?, except: [],
only: [],
except: [],
) )
failed_casks = results.reject { |_, result| result[:errors].empty? }.map(&:first) failed_casks = results.reject { |_, result| result[:errors].empty? }.map(&:first)
@ -81,8 +77,6 @@ module Cask
quarantine:, quarantine:,
any_named_args:, any_named_args:,
language:, language:,
display_passes:,
display_failures_only:,
only:, only:,
except: except:
) )
@ -96,8 +90,6 @@ module Cask
quarantine: quarantine, quarantine: quarantine,
language: language, language: language,
any_named_args: any_named_args, any_named_args: any_named_args,
display_passes: display_passes,
display_failures_only: display_failures_only,
only: only, only: only,
except: except, except: except,
}.compact }.compact
@ -110,7 +102,7 @@ module Cask
casks.to_h do |cask| casks.to_h do |cask|
odebug "Auditing Cask #{cask}" odebug "Auditing Cask #{cask}"
[cask.sourcefile_path, Auditor.audit(cask, **options)] [cask.sourcefile_path, { errors: Auditor.audit(cask, **options), warnings: [] }]
end end
end end
end end

View File

@ -42,6 +42,8 @@ module Cask
quarantine = true if quarantine.nil? quarantine = true if quarantine.nil?
greedy = true if Homebrew::EnvConfig.upgrade_greedy?
outdated_casks = if casks.empty? outdated_casks = if casks.empty?
Caskroom.casks(config: Config.from_args(args)).select do |cask| Caskroom.casks(config: Config.from_args(args)).select do |cask|
cask.outdated?(greedy: greedy, greedy_latest: greedy_latest, cask.outdated?(greedy: greedy, greedy_latest: greedy_latest,

View File

@ -65,7 +65,8 @@ module Homebrew
description: "Prefix every line of output with the file or formula name being audited, to " \ description: "Prefix every line of output with the file or formula name being audited, to " \
"make output easy to grep." "make output easy to grep."
switch "--display-failures-only", switch "--display-failures-only",
description: "Only display casks that fail the audit. This is the default for formulae." description: "Only display casks that fail the audit. This is the default for formulae and casks.",
hidden: true
switch "--skip-style", switch "--skip-style",
description: "Skip running non-RuboCop style checks. Useful if you plan on running " \ description: "Skip running non-RuboCop style checks. Useful if you plan on running " \
"`brew style` separately. Enabled by default unless a formula is specified by name." "`brew style` separately. Enabled by default unless a formula is specified by name."
@ -242,24 +243,26 @@ module Homebrew
require "cask/cmd/abstract_command" require "cask/cmd/abstract_command"
require "cask/cmd/audit" require "cask/cmd/audit"
if args.display_failures_only?
odeprecated "`brew audit <cask> --display-failures-only`", "`brew audit <cask>` without the argument"
end
# For switches, we add `|| nil` so that `nil` will be passed instead of `false` if they aren't set. # For switches, we add `|| nil` so that `nil` will be passed instead of `false` if they aren't set.
# This way, we can distinguish between "not set" and "set to false". # This way, we can distinguish between "not set" and "set to false".
Cask::Cmd::Audit.audit_casks( Cask::Cmd::Audit.audit_casks(
*audit_casks, *audit_casks,
download: nil, download: nil,
# No need for `|| nil` for `--[no-]signing` because boolean switches are already `nil` if not passed # No need for `|| nil` for `--[no-]signing` because boolean switches are already `nil` if not passed
signing: args.signing?, signing: args.signing?,
online: args.online? || nil, online: args.online? || nil,
strict: args.strict? || nil, strict: args.strict? || nil,
new_cask: args.new_cask? || nil, new_cask: args.new_cask? || nil,
token_conflicts: args.token_conflicts? || nil, token_conflicts: args.token_conflicts? || nil,
quarantine: nil, quarantine: nil,
any_named_args: !no_named_args, any_named_args: !no_named_args,
language: nil, language: nil,
display_passes: args.verbose? || args.named.count == 1, only: args.only,
display_failures_only: args.display_failures_only?, except: args.except,
only: args.only,
except: args.except,
) )
end end
@ -267,7 +270,7 @@ module Homebrew
cask_count = failed_casks.count cask_count = failed_casks.count
cask_problem_count = failed_casks.sum { |_, result| result[:warnings].count + result[:errors].count } cask_problem_count = failed_casks.sum { |_, result| result.count }
new_formula_problem_count += new_formula_problem_lines.count new_formula_problem_count += new_formula_problem_lines.count
total_problems_count = problem_count + new_formula_problem_count + cask_problem_count + tap_problem_count total_problems_count = problem_count + new_formula_problem_count + cask_problem_count + tap_problem_count

View File

@ -463,15 +463,10 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy
url = url.sub(%r{^https?://#{GitHubPackages::URL_DOMAIN}/}o, "#{domain.chomp("/")}/") url = url.sub(%r{^https?://#{GitHubPackages::URL_DOMAIN}/}o, "#{domain.chomp("/")}/")
end end
output, _, _status = curl_output( parsed_output = curl_head(url.to_s, timeout: timeout)
"--location", "--silent", "--head", "--request", "GET", url.to_s, parsed_headers = parsed_output.fetch(:responses).map { |r| r.fetch(:headers) }
timeout: timeout
)
parsed_output = parse_curl_output(output)
lines = output.to_s.lines.map(&:chomp) final_url = curl_response_follow_redirections(parsed_output.fetch(:responses), url)
final_url = curl_response_follow_redirections(parsed_output[:responses], url)
content_disposition_parser = Mechanize::HTTP::ContentDispositionParser.new content_disposition_parser = Mechanize::HTTP::ContentDispositionParser.new
@ -500,19 +495,20 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy
File.basename(filename) File.basename(filename)
end end
filenames = lines.map(&parse_content_disposition).compact filenames = parsed_headers.flat_map do |headers|
next [] unless (header = headers["content-disposition"])
time = [*parse_content_disposition.call("Content-Disposition: #{header}")]
lines.map { |line| line[/^Last-Modified:\s*(.+)/i, 1] } end
.compact
time = parsed_headers
.flat_map { |headers| [*headers["last-modified"]] }
.map { |t| t.match?(/^\d+$/) ? Time.at(t.to_i) : Time.parse(t) } .map { |t| t.match?(/^\d+$/) ? Time.at(t.to_i) : Time.parse(t) }
.last .last
file_size = file_size = parsed_headers
lines.map { |line| line[/^Content-Length:\s*(\d+)/i, 1] } .flat_map { |headers| [*headers["content-length"]&.to_i] }
.compact .last
.map(&:to_i)
.last
is_redirection = url != final_url is_redirection = url != final_url
basename = filenames.last || parse_basename(final_url, search_query: !is_redirection) basename = filenames.last || parse_basename(final_url, search_query: !is_redirection)

View File

@ -344,6 +344,10 @@ module Homebrew
description: "If set, use Pry for the `brew irb` command.", description: "If set, use Pry for the `brew irb` command.",
boolean: true, boolean: true,
}, },
HOMEBREW_UPGRADE_GREEDY: {
description: "If set, pass `--greedy` to all cask upgrade commands.",
boolean: true,
},
HOMEBREW_SIMULATE_MACOS_ON_LINUX: { HOMEBREW_SIMULATE_MACOS_ON_LINUX: {
description: "If set, running Homebrew on Linux will simulate certain macOS code paths. This is useful " \ description: "If set, running Homebrew on Linux will simulate certain macOS code paths. This is useful " \
"when auditing macOS formulae while on Linux.", "when auditing macOS formulae while on Linux.",

View File

@ -229,6 +229,9 @@ module Homebrew::EnvConfig
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def self.update_to_tag?; end def self.update_to_tag?; end
sig { returns(T::Boolean) }
def self.upgrade_greedy?; end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def self.verbose?; end def self.verbose?; end

View File

@ -498,7 +498,7 @@ class Tap
formula_dir.find formula_dir.find
else else
formula_dir.children formula_dir.children
end.select(&method(:ruby_file?)) end.select(&method(:formula_file?))
else else
[] []
end end
@ -561,7 +561,7 @@ class Tap
file.extname == ".rb" file.extname == ".rb"
end end
# return true if given path would present a {Formula} file in this {Tap}. # returns true if given path would present a {Formula} file in this {Tap}.
# accepts both absolute path and relative path (relative to this {Tap}'s path) # accepts both absolute path and relative path (relative to this {Tap}'s path)
# @private # @private
sig { params(file: T.any(String, Pathname)).returns(T::Boolean) } sig { params(file: T.any(String, Pathname)).returns(T::Boolean) }
@ -569,11 +569,12 @@ class Tap
file = Pathname.new(file) unless file.is_a? Pathname file = Pathname.new(file) unless file.is_a? Pathname
file = file.expand_path(path) file = file.expand_path(path)
return false unless ruby_file?(file) return false unless ruby_file?(file)
return false if cask_file?(file)
file.to_s.start_with?("#{formula_dir}/") file.to_s.start_with?("#{formula_dir}/")
end end
# return true if given path would present a {Cask} file in this {Tap}. # returns true if given path would present a {Cask} file in this {Tap}.
# accepts both absolute path and relative path (relative to this {Tap}'s path) # accepts both absolute path and relative path (relative to this {Tap}'s path)
# @private # @private
sig { params(file: T.any(String, Pathname)).returns(T::Boolean) } sig { params(file: T.any(String, Pathname)).returns(T::Boolean) }

View File

@ -13,23 +13,14 @@ describe Cask::Audit, :cask do
end end
def passed?(audit) def passed?(audit)
!audit.errors? && !audit.warnings? !audit.errors?
end end
def outcome(audit) def outcome(audit)
if passed?(audit) if passed?(audit)
"passed" "passed"
else else
message = "" "errored with #{audit.errors.map { |e| e.fetch(:message).inspect }.join(",")}"
message += "warned with #{audit.warnings.map { |e| e.fetch(:message).inspect }.join(",")}" if audit.warnings?
if audit.errors?
message += " and " if audit.warnings?
message += "errored with #{audit.errors.map { |e| e.fetch(:message).inspect }.join(",")}"
end
message
end end
end end
@ -53,16 +44,6 @@ describe Cask::Audit, :cask do
end end
end end
matcher :warn_with do |message|
match do |audit|
include_msg?(audit.warnings, message)
end
failure_message do |audit|
"expected to warn with message #{message.inspect} but #{outcome(audit)}"
end
end
let(:cask) { instance_double(Cask::Cask) } let(:cask) { instance_double(Cask::Cask) }
let(:new_cask) { nil } let(:new_cask) { nil }
let(:online) { nil } let(:online) { nil }
@ -118,6 +99,14 @@ describe Cask::Audit, :cask do
describe "#result" do describe "#result" do
subject { audit.result } subject { audit.result }
context "when there are no errors and `--strict` is not passed so we should not show anything" do
before do
audit.add_error("eh", strict_only: true)
end
it { is_expected.not_to match(/failed/) }
end
context "when there are errors" do context "when there are errors" do
before do before do
audit.add_error "bad" audit.add_error "bad"
@ -126,25 +115,42 @@ describe Cask::Audit, :cask do
it { is_expected.to match(/failed/) } it { is_expected.to match(/failed/) }
end end
context "when there are warnings" do
before do
audit.add_warning "eh"
end
it { is_expected.to match(/warning/) }
end
context "when there are errors and warnings" do context "when there are errors and warnings" do
before do before do
audit.add_error "bad" audit.add_error "bad"
audit.add_warning "eh" audit.add_error("eh", strict_only: true)
end end
it { is_expected.to match(/failed/) } it { is_expected.to match(/failed/) }
end end
context "when there are no errors or warnings" do context "when there are errors and warnings and `--strict` is passed" do
it { is_expected.to match(/passed/) } let(:strict) { true }
before do
audit.add_error "very bad"
audit.add_error("a little bit bad", strict_only: true)
end
it { is_expected.to match(/failed/) }
end
context "when there are warnings and `--strict` is not passed" do
before do
audit.add_error("a little bit bad", strict_only: true)
end
it { is_expected.not_to match(/failed/) }
end
context "when there are warnings and `--strict` is passed" do
let(:strict) { true }
before do
audit.add_error("a little bit bad", strict_only: true)
end
it { is_expected.to match(/failed/) }
end end
end end
@ -485,7 +491,7 @@ describe Cask::Audit, :cask do
it "does not fail" do it "does not fail" do
expect(download_double).not_to receive(:fetch) expect(download_double).not_to receive(:fetch)
expect(UnpackStrategy).not_to receive(:detect) expect(UnpackStrategy).not_to receive(:detect)
expect(run).not_to warn_with(/Audit\.app/) expect(run).not_to error_with(/Audit\.app/)
end end
end end
@ -503,7 +509,7 @@ describe Cask::Audit, :cask do
it "does not fail since no extract" do it "does not fail since no extract" do
allow(download_double).to receive(:fetch).and_return(Pathname.new("/tmp/test.zip")) allow(download_double).to receive(:fetch).and_return(Pathname.new("/tmp/test.zip"))
allow(UnpackStrategy).to receive(:detect).and_return(nil) allow(UnpackStrategy).to receive(:detect).and_return(nil)
expect(run).not_to warn_with(/Audit\.app/) expect(run).not_to error_with(/Audit\.app/)
end end
end end
end end
@ -984,9 +990,20 @@ describe Cask::Audit, :cask do
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] }
it "warns about duplicates" do context "when `--strict` is passed" do
expect(audit).to receive(:core_formula_names).and_return(formula_names) let(:strict) { true }
expect(run).to warn_with(/possible duplicate/)
it "warns about duplicates" do
expect(audit).to receive(:core_formula_names).and_return(formula_names)
expect(run).to error_with(/possible duplicate/)
end
end
context "when `--strict` is not passed" do
it "does not warn about duplicates" do
expect(audit).to receive(:core_formula_names).and_return(formula_names)
expect(run).not_to error_with(/possible duplicate/)
end
end end
end end
@ -1058,8 +1075,8 @@ describe Cask::Audit, :cask do
context "when `new_cask` is false" do context "when `new_cask` is false" do
let(:new_cask) { false } let(:new_cask) { false }
it "warns" do it "does not warn" do
expect(run).to warn_with(/should have a description/) expect(run).not_to error_with(/should have a description/)
end end
end end

View File

@ -6,7 +6,7 @@ require "cask/auditor"
describe Cask::Cmd::Audit, :cask do describe Cask::Cmd::Audit, :cask do
let(:cask) { Cask::Cask.new("cask") } let(:cask) { Cask::Cask.new("cask") }
let(:cask_with_many_languages) { Cask::CaskLoader.load(cask_path("with-many-languages")) } let(:cask_with_many_languages) { Cask::CaskLoader.load(cask_path("with-many-languages")) }
let(:result) { { warnings: Set.new, errors: Set.new } } let(:result) { Set.new }
describe "selection of Casks to audit" do describe "selection of Casks to audit" do
it "audits all Casks if no tokens are given" do it "audits all Casks if no tokens are given" do
@ -25,7 +25,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_new_cask: false, quarantine: true, any_named_args: true, audit_new_cask: false, quarantine: true, any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)
@ -40,7 +39,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_new_cask: false, quarantine: true, any_named_args: true, audit_new_cask: false, quarantine: true, any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)
@ -54,7 +52,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_download: true, audit_new_cask: false, quarantine: true, any_named_args: true, audit_download: true, audit_new_cask: false, quarantine: true, any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)
@ -68,7 +65,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_token_conflicts: true, audit_new_cask: false, quarantine: true, any_named_args: true, audit_token_conflicts: true, audit_new_cask: false, quarantine: true, any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)
@ -82,7 +78,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_strict: true, audit_new_cask: false, quarantine: true, any_named_args: true, audit_strict: true, audit_new_cask: false, quarantine: true, any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)
@ -96,7 +91,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_online: true, audit_new_cask: false, quarantine: true, any_named_args: true, audit_online: true, audit_new_cask: false, quarantine: true, any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)
@ -110,7 +104,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_new_cask: true, quarantine: true, any_named_args: true, audit_new_cask: true, quarantine: true, any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)
@ -124,7 +117,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_new_cask: false, quarantine: true, language: ["de-AT"], any_named_args: true, audit_new_cask: false, quarantine: true, language: ["de-AT"], any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)
@ -138,7 +130,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_new_cask: false, quarantine: false, any_named_args: true, audit_new_cask: false, quarantine: false, any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)
@ -154,7 +145,6 @@ describe Cask::Cmd::Audit, :cask do
.with( .with(
cask, cask,
audit_new_cask: false, quarantine: false, any_named_args: true, audit_new_cask: false, quarantine: false, any_named_args: true,
display_failures_only: false, display_passes: true,
only: [], except: [] only: [], except: []
) )
.and_return(result) .and_return(result)

View File

@ -39,9 +39,7 @@ describe Cask::Quarantine, :cask do
it "quarantines Cask audits" do it "quarantines Cask audits" do
expect do expect do
Cask::Cmd::Audit.run("local-transmission", "--download") Cask::Cmd::Audit.run("local-transmission", "--download")
end.to not_raise_error end.to not_raise_error.and not_to_output.to_stderr
.and output(/audit for local-transmission: passed/).to_stdout
.and not_to_output.to_stderr
local_transmission = Cask::CaskLoader.load(cask_path("local-transmission")) local_transmission = Cask::CaskLoader.load(cask_path("local-transmission"))
cached_location = Cask::Download.new(local_transmission).fetch cached_location = Cask::Download.new(local_transmission).fetch
@ -156,9 +154,7 @@ describe Cask::Quarantine, :cask do
it "does not quarantine Cask audits" do it "does not quarantine Cask audits" do
expect do expect do
Cask::Cmd::Audit.run("local-transmission", "--download", "--no-quarantine") Cask::Cmd::Audit.run("local-transmission", "--download", "--no-quarantine")
end.to not_raise_error end.to not_raise_error.and not_to_output.to_stderr
.and output(/audit for local-transmission: passed/).to_stdout
.and not_to_output.to_stderr
local_transmission = Cask::CaskLoader.load(cask_path("local-transmission")) local_transmission = Cask::CaskLoader.load(cask_path("local-transmission"))
cached_location = Cask::Download.new(local_transmission).fetch cached_location = Cask::Download.new(local_transmission).fetch

View File

@ -9,12 +9,38 @@ describe CurlGitHubPackagesDownloadStrategy do
let(:name) { "foo" } let(:name) { "foo" }
let(:url) { "https://#{GitHubPackages::URL_DOMAIN}/v2/homebrew/core/spec_test/manifests/1.2.3" } let(:url) { "https://#{GitHubPackages::URL_DOMAIN}/v2/homebrew/core/spec_test/manifests/1.2.3" }
let(:version) { "1.2.3" } let(:version) { "1.2.3" }
let(:specs) { {} } let(:specs) { { headers: ["Accept: application/vnd.oci.image.index.v1+json"] } }
let(:authorization) { nil } let(:authorization) { nil }
let(:head_response) do
<<~HTTP
HTTP/2 200\r
content-length: 12671\r
content-type: application/vnd.oci.image.index.v1+json\r
docker-content-digest: sha256:7d752ee92d9120e3884b452dce15328536a60d468023ea8e9f4b09839a5442e5\r
docker-distribution-api-version: registry/2.0\r
etag: "sha256:7d752ee92d9120e3884b452dce15328536a60d468023ea8e9f4b09839a5442e5"\r
date: Sun, 02 Apr 2023 22:45:08 GMT\r
x-github-request-id: 8814:FA5A:14DAFB5:158D7A2:642A0574\r
HTTP
end
describe "#fetch" do describe "#fetch" do
before do before do
stub_const("HOMEBREW_GITHUB_PACKAGES_AUTH", authorization) if authorization.present? stub_const("HOMEBREW_GITHUB_PACKAGES_AUTH", authorization) if authorization.present?
allow(strategy).to receive(:system_command)
.with(
/curl/,
hash_including(args: array_including("--head")),
)
.twice
.and_return(instance_double(
SystemCommand::Result,
success?: true,
exit_status: instance_double(Process::Status, exitstatus: 0),
stdout: head_response,
))
strategy.temporary_path.dirname.mkpath strategy.temporary_path.dirname.mkpath
FileUtils.touch strategy.temporary_path FileUtils.touch strategy.temporary_path
end end

View File

@ -10,9 +10,28 @@ describe CurlPostDownloadStrategy do
let(:url) { "https://example.com/foo.tar.gz" } let(:url) { "https://example.com/foo.tar.gz" }
let(:version) { "1.2.3" } let(:version) { "1.2.3" }
let(:specs) { {} } let(:specs) { {} }
let(:head_response) do
<<~HTTP
HTTP/1.1 200\r
Content-Disposition: attachment; filename="foo.tar.gz"
HTTP
end
describe "#fetch" do describe "#fetch" do
before do before do
allow(strategy).to receive(:system_command)
.with(
/curl/,
hash_including(args: array_including("--head")),
)
.twice
.and_return(instance_double(
SystemCommand::Result,
success?: true,
exit_status: instance_double(Process::Status, exitstatus: 0),
stdout: head_response,
))
strategy.temporary_path.dirname.mkpath strategy.temporary_path.dirname.mkpath
FileUtils.touch strategy.temporary_path FileUtils.touch strategy.temporary_path
end end

View File

@ -12,6 +12,10 @@ describe CurlDownloadStrategy do
let(:specs) { { user: "download:123456" } } let(:specs) { { user: "download:123456" } }
let(:artifact_domain) { nil } let(:artifact_domain) { nil }
before do
allow(strategy).to receive(:curl_head).and_return({ responses: [{ headers: {} }] })
end
it "parses the opts and sets the corresponding args" do it "parses the opts and sets the corresponding args" do
expect(strategy.send(:_curl_args)).to eq(["--user", "download:123456"]) expect(strategy.send(:_curl_args)).to eq(["--user", "download:123456"])
end end
@ -190,48 +194,48 @@ describe CurlDownloadStrategy do
end end
describe "#cached_location" do describe "#cached_location" do
subject(:cached_location) { described_class.new(url, name, version, **specs).cached_location } subject(:cached_location) { strategy.cached_location }
context "when URL ends with file" do context "when URL ends with file" do
it { it "falls back to the file name in the URL" do
expect(cached_location).to eq( expect(cached_location).to eq(
HOMEBREW_CACHE/"downloads/3d1c0ae7da22be9d83fb1eb774df96b7c4da71d3cf07e1cb28555cf9a5e5af70--foo.tar.gz", HOMEBREW_CACHE/"downloads/3d1c0ae7da22be9d83fb1eb774df96b7c4da71d3cf07e1cb28555cf9a5e5af70--foo.tar.gz",
) )
} end
end end
context "when URL file is in middle" do context "when URL file is in middle" do
let(:url) { "https://example.com/foo.tar.gz/from/this/mirror" } let(:url) { "https://example.com/foo.tar.gz/from/this/mirror" }
it { it "falls back to the file name in the URL" do
expect(cached_location).to eq( expect(cached_location).to eq(
HOMEBREW_CACHE/"downloads/1ab61269ba52c83994510b1e28dd04167a2f2e8393a35a9c50c1f7d33fd8f619--foo.tar.gz", HOMEBREW_CACHE/"downloads/1ab61269ba52c83994510b1e28dd04167a2f2e8393a35a9c50c1f7d33fd8f619--foo.tar.gz",
) )
} end
end end
context "with a file name trailing the URL path" do context "with a file name trailing the URL path" do
let(:url) { "https://example.com/cask.dmg" } let(:url) { "https://example.com/cask.dmg" }
it { it "falls back to the file extension in the URL" do
expect(cached_location.extname).to eq(".dmg") expect(cached_location.extname).to eq(".dmg")
} end
end end
context "with a file name trailing the first query parameter" do context "with a file name trailing the first query parameter" do
let(:url) { "https://example.com/download?file=cask.zip&a=1" } let(:url) { "https://example.com/download?file=cask.zip&a=1" }
it { it "falls back to the file extension in the URL" do
expect(cached_location.extname).to eq(".zip") expect(cached_location.extname).to eq(".zip")
} end
end end
context "with a file name trailing the second query parameter" do context "with a file name trailing the second query parameter" do
let(:url) { "https://example.com/dl?a=1&file=cask.zip&b=2" } let(:url) { "https://example.com/dl?a=1&file=cask.zip&b=2" }
it { it "falls back to the file extension in the URL" do
expect(cached_location.extname).to eq(".zip") expect(cached_location.extname).to eq(".zip")
} end
end end
context "with an unusually long query string" do context "with an unusually long query string" do
@ -253,10 +257,10 @@ describe CurlDownloadStrategy do
].join ].join
end end
it { it "falls back to the file extension in the URL" do
expect(cached_location.extname).to eq(".zip") expect(cached_location.extname).to eq(".zip")
expect(cached_location.to_path.length).to be_between(0, 255) expect(cached_location.to_path.length).to be_between(0, 255)
} end
end end
end end
end end

View File

@ -141,7 +141,7 @@ module Utils
raise Timeout::Error, result.stderr.lines.last.chomp if timeout && result.status.exitstatus == 28 raise Timeout::Error, result.stderr.lines.last.chomp if timeout && result.status.exitstatus == 28
# Error in the HTTP2 framing layer # Error in the HTTP2 framing layer
if result.status.exitstatus == 16 if result.exit_status == 16
return curl_with_workarounds( return curl_with_workarounds(
*args, "--http1.1", *args, "--http1.1",
timeout: end_time&.remaining, **command_options, **options timeout: end_time&.remaining, **command_options, **options
@ -149,7 +149,7 @@ module Utils
end end
# This is a workaround for https://github.com/curl/curl/issues/1618. # This is a workaround for https://github.com/curl/curl/issues/1618.
if result.status.exitstatus == 56 # Unexpected EOF if result.exit_status == 56 # Unexpected EOF
out = curl_output("-V").stdout out = curl_output("-V").stdout
# If `curl` doesn't support HTTP2, the exception is unrelated to this bug. # If `curl` doesn't support HTTP2, the exception is unrelated to this bug.
@ -207,6 +207,28 @@ module Utils
curl_with_workarounds(*args, print_stderr: false, show_output: true, **options) curl_with_workarounds(*args, print_stderr: false, show_output: true, **options)
end end
def curl_head(*args, **options)
[[], ["--request", "GET"]].each do |request_args|
result = curl_output(
"--fail", "--location", "--silent", "--head", *request_args, *args,
**options
)
# 22 means a non-successful HTTP status code, not a `curl` error, so we still got some headers.
if result.success? || result.exit_status == 22
parsed_output = parse_curl_output(result.stdout)
# If we didn't get a `Content-Disposition` header yet, retry using `GET`.
next if request_args.empty? &&
parsed_output.fetch(:responses).none? { |r| r.fetch(:headers).key?("content-disposition") }
return parsed_output if result.success?
end
result.assert_success!
end
end
# Check if a URL is protected by CloudFlare (e.g. badlion.net and jaxx.io). # Check if a URL is protected by CloudFlare (e.g. badlion.net and jaxx.io).
# @param response [Hash] A response hash from `#parse_curl_response`. # @param response [Hash] A response hash from `#parse_curl_response`.
# @return [true, false] Whether a response contains headers indicating that # @return [true, false] Whether a response contains headers indicating that

View File

@ -29,14 +29,13 @@ module Utils
# defined by the formula, as only `HOMEBREW_PREFIX` is available # defined by the formula, as only `HOMEBREW_PREFIX` is available
# in the {DATAPatch embedded patch}. # in the {DATAPatch embedded patch}.
# #
# `inreplace` supports regular expressions: # @example `inreplace` supports regular expressions:
# <pre>inreplace "somefile.cfg", /look[for]what?/, "replace by #{bin}/tool"</pre> # inreplace "somefile.cfg", /look[for]what?/, "replace by #{bin}/tool"
# #
# `inreplace` supports blocks: # @example `inreplace` supports blocks:
# <pre>inreplace "Makefile" do |s| # inreplace "Makefile" do |s|
# s.gsub! "/usr/local", HOMEBREW_PREFIX.to_s # s.gsub! "/usr/local", HOMEBREW_PREFIX.to_s
# end # end
# </pre>
# #
# @see StringInreplaceExtension # @see StringInreplaceExtension
# @api public # @api public

View File

@ -30,7 +30,7 @@ $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/zeitwerk-2.6.7/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/zeitwerk-2.6.7/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/activesupport-6.1.7.3/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/activesupport-6.1.7.3/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/public_suffix-5.0.1/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/public_suffix-5.0.1/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/addressable-2.8.2/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/addressable-2.8.3/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/ast-2.4.2/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/ast-2.4.2/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/bindata-2.4.15/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/bindata-2.4.15/lib")
$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/universal-darwin-21/#{Gem.extension_api_version}/msgpack-1.7.0") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/universal-darwin-21/#{Gem.extension_api_version}/msgpack-1.7.0")

View File

@ -896,19 +896,16 @@ module Addressable
# #
# @return [Hash, Array, String] The normalized values # @return [Hash, Array, String] The normalized values
def normalize_value(value) def normalize_value(value)
unless value.is_a?(Hash)
value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str
end
# Handle unicode normalization # Handle unicode normalization
if value.kind_of?(Array) if value.respond_to?(:to_ary)
value.map! { |val| normalize_value(val) } value.to_ary.map! { |val| normalize_value(val) }
elsif value.kind_of?(Hash) elsif value.kind_of?(Hash)
value = value.inject({}) { |acc, (k, v)| value = value.inject({}) { |acc, (k, v)|
acc[normalize_value(k)] = normalize_value(v) acc[normalize_value(k)] = normalize_value(v)
acc acc
} }
else else
value = value.to_s if !value.kind_of?(String)
if value.encoding != Encoding::UTF_8 if value.encoding != Encoding::UTF_8
value = value.dup.force_encoding(Encoding::UTF_8) value = value.dup.force_encoding(Encoding::UTF_8)
end end

View File

@ -23,7 +23,7 @@ if !defined?(Addressable::VERSION)
module VERSION module VERSION
MAJOR = 2 MAJOR = 2
MINOR = 8 MINOR = 8
TINY = 2 TINY = 3
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View File

@ -1,7 +1,10 @@
# typed: false # typed: true
# frozen_string_literal: true # frozen_string_literal: true
def init def init
# `sorbet` is available transitively through the `yard-sorbet` plugin, but we're
# outside of the standalone sorbet config, so `checked` is enabled by default
T.bind(self, YARD::Templates::Template, checked: false)
super super
return if sections.empty? return if sections.empty?
@ -10,5 +13,6 @@ def init
end end
def internal def internal
T.bind(self, YARD::Templates::Template, checked: false)
erb(:internal) if object.has_tag?(:api) && object.tag(:api).text == "internal" erb(:internal) if object.has_tag?(:api) && object.tag(:api).text == "internal"
end end

162
docs/How-To-Organize-AGM.md Normal file
View File

@ -0,0 +1,162 @@
# How to Organize AGM
AGM is our combination of business meeting, yearly work planning session, and opportunity to meet others in our international team in person.
This document is a _guide_ that assumes that the meeting will be held in person.
If a situation occurs that prevents that, it is acceptable to execute it virtually, as was done in 2021 and 2022 during the COVID-19 pandemic.
<!-- TOC start -->
* [Roles](#roles)
* [Logistics Timeline](#logistics-timeline)
* [Three months prior](#three-months-prior)
* [Two months prior](#two-months-prior)
* [Four weeks prior](#four-weeks-prior)
* [Three weeks prior](#three-weeks-prior)
* [Two weeks prior](#two-weeks-prior)
* [10 days prior](#10-days-prior)
* [One week prior](#one-week-prior)
* [Day before](#day-before)
* [Day-of](#day-of)
* [Pre-planning](#pre-planning)
* [Finding a Meeting Venue](#finding-a-meeting-venue)
* [Who Qualifies For AGM Travel Assistance](#who-qualifies-for-agm-travel-assistance)
* [Ideas for future AGMs](#ideas-for-future-agms)
* [Meeting enhancements](#meeting-enhancements)
* [Day-of enhancements](#day-of-enhancements)
<!-- TOC end -->
## Roles
Expected participants:
|Who|Role|
|---|---|
|Project Leadership Committee (PLC)|Should be physically present if possible, dialed-in if not. Several members must be present in person to run the event. Several members, regardless, needed to provide content for meeting.|
|Project Leader (PL)|Should be physically present if possible, dialed-in if not. Regardless, needed to provide content for meeting.|
|Technology Steering Committee (TSC)|Should be physically present if possible, dialed-in if not. Regardless, needed to provide content for meeting.|
|Members|Should dial-in or participate in person if possible.|
PLC members' roles of responsibility for planning and execution:
|Who|Role|
|---|---|
|Logistics Coordinator (LC)|Coordinates with meeting venue, restaurants, members, committees, vendors|
|Agenda Coordinator (AC)|Coordinates agenda and content to be presented|
|Technology Coordinator (TC)|Coordinates video conference audiovisual setup|
:information_source: _(A person may have more than one role but one person should not have all roles.)_
## Logistics Timeline
Past practice and future intent is for AGM to coincide with [FOSDEM](https://fosdem.org "Free and Open Source Developers European Meeting"), which is held in Brussels, Belgium annually typically on the Saturday and Sunday of the fifth ISO-8601 week of the calendar year, calculable with:
ruby -rdate -e "s=ARGV[0].to_i;s.upto(s+4).map{|y|Date.commercial(y,5,6)}.each{|y|puts [y,y+1].join(' - ')}" 2024
AGM should be held on the Friday before or the Monday following FOSDEM.
:information_source: _Regenerate the dates for the WHEN lines in the next several headers
using this quick command:_
ruby -rdate -e "YEAR=ARGV[0].to_i;puts ([[44,YEAR-1],[49,YEAR-1]]+(1.upto(4).map{|wk|[wk, YEAR]})).map{|wk,yr|Date.commercial(yr,wk).to_s}" 2024
### Three months prior
**When:** Week 44 of YEAR-1 :date: `2023-10-30`
* [ ] LC: Seek venue through previous contacts or RFP.
* [ ] PLC: Notify members of eligibility to attend AGM, with date to be determined.
* This is primarily to enable members to begin planning travel by
asking for time off, requesting employer reimbursement,
arranging childcare or pet sitters,
[applying for a visa](https://5195.f2w.bosa.be/en/themes/entry/border-control/visa/visa-type-c)
which may [take 27 weeks](https://dofi.ibz.be/en/themes/third-country-nationals/short-stay/processing-time-visa-application),
etc.
### Two months prior
**When:** Week 49 of YEAR-1 :date: `2023-12-04`
* [ ] LC: Seek informal count of members intending to attend in-person.
* [ ] PL: Review maintainer activity per [Governance/Maintainers](Homebrew-Governance.md#8-maintainers).
* [ ] PLC: Determine travel assistance budget.
* [ ] PLC: Open travel assistance pre-approval process.
### Four weeks prior
**When:** Week 1 of YEAR :date: `2024-01-01`
* [ ] PLC: Solicit changes to [Homebrew Governance](Homebrew-Governance.md) in the form of PRs on the `homebrew-governance-private` repo.
### Three weeks prior
**When:** Week 2 of YEAR :date: `2024-01-08`
* [ ] PLC: Close travel assistance pre-approval process.
### Two weeks prior
**When:** Week 3 of YEAR :date: `2024-01-15`
* [ ] AC: Create agenda, solicit agenda items from PLC and TSC.
* [ ] LC: Seek committed member attendance and dietary requirements for each.
* [ ] PLC: Close proposals for new Governance changes.
### 10 days prior
**When:** Week 4 of YEAR :date: `2024-01-22`
* [ ] PLC: Resolve all open Governance PRs, roll-up changes, and open PR with changes to `docs/Homebrew-Governance.md` on `homebrew/brew`.
### One week prior
**When:** Week 4 of YEAR :date: `2024-01-22`
* [ ] PLC: Open voting for PLC, PL, and Governance changes.
* [ ] AC: Solicit agenda items from membership.
* [ ] LC: Secure a venue and reservation for dinner
### Day before
* [ ] LC: Confirm reservation count for dinner with attendees
* [ ] LC: Hand-off venue AV contact to TC
### Day-of
* [ ] LC: Confirm reservation count for dinner with venue
* [ ] TC: Connect to video conference, ensure audiovisual equipment is ready and appropriately placed and leveled periodically
* [ ] AC: Keep the meeting paced to the agenda, keep time for timeboxed discussions, cut people off if they're talking too long, ensure remote attendees can get a word in
## Pre-planning
### Finding a Meeting Venue
In the past, PLC hosted the AGM at the
[THON Hotel Brussels City Centre](https://www.thonhotels.com/conference/belgium/brussels/thon-hotel-brussels-city-centre/?Persons=20)
and arranged for a room block checking in the day before FOSDEM and AGM weekend, generally on Friday, and checking out the day after, generally Tuesday when the AGM is Monday.
### Who Qualifies For AGM Travel Assistance
Travel assistance is available for AGM participants who are expected to attend the AGM in-person.
Those who have employers able to cover all or a part of the costs of attending FOSDEM should exhaust that
source of funding before seeking Homebrew funding.
PLC, TSC, PL and maintainers can expect to have all reasonable, in-policy expenses covered while members will be considered on a case-by-case basis.
See also the [Reimbursement Policy](Expense-and-Reimbursement-Policy.md#travel) for process and details on what is covered.
It is important that all attendees expecting reimbursement stay in-policy.
## Ideas for future AGMs
### Meeting enhancements
* Captioning or transcription, or both - [White Coat Captioning](https://whitecoatcaptioning.com) could handle the live captioning and provide us that for a transcript.
* Separate meeting runner
* Keep PL ideally focused on content and not agenda or tracking who's asked to speak
* Should be a PLC member who is not the AC, LC, or TC
* Should be someone happy and willing to cut people off mid-sentence and, assertively but in a friendly manner, stop conversations that are not running to time
### Day-of enhancements
* Track dietary requirements centrally for in-person participants

View File

@ -0,0 +1,45 @@
# Homebrew Annual General Meeting 2022
## Minutes
- 09:10 Call to order
## Reports
- 09:15 Project Leader Report (15 min, Mike McQuaid)
- 09:40 Project Leadership Committee Report (15 min, Sean Molenaar)
- 10:00 Technical Steering Commitee Report (15 min, Michka Popoff)
## Election Results
- 10:00 PL Voting Results (5 min, Issy Long)
- Mike McQuaid was re-elected.
- 10:05 PLC Voting Results (5 min, Sean Molenaar because Issy felt weird announcing their own election)
- Issy Long and Jon Chang were re-elected and Colin Dean was newly elected.
- Commiserations to George Adams who was not elected.
- 10:10 Governance Changes Voting Results (5 min, Issy Long)
- The governance changes (<https://github.com/Homebrew/brew/pull/14482>) passed.
## Member presentations and discussions (11:00-12:00 and 13:30-15:00)
- ARM on Linux (Michka Popoff)
- Install from API (Rylan Polster)
- Homebrew on Linux (Patrick Linanne and Bo Anderson)
- CI infrastructure (Rui Chen)
- Security initiatives (Patrick Linanne)
- Formula licenses (Rui Chen)
- GitHub authentication security improvements (Colin Dean)
- Improvements to autobumping formulae and casks (Rui Chen)
- Formalizing the reimbursement process (Colin Dean)
- Homebrew Mastodon account (Mike McQuaid)
- InfluxDB analytics (Mike McQuaid)
- Security posture improvements, potentially making a security-focused volunteer group (Mike McQuaid)
## Hackathon (undocumented, 15:00-17:00)
## Adjournment
- Final remarks, thanks for coming (Issy Long)
- 17:00 Meeting finished.