From bc295f79476e1218701fe5c2b8d39a8a9cf74842 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Wed, 7 Sep 2022 16:49:41 +0100 Subject: [PATCH] update-sponsors: don't require admin token. Instead, use a different API to query these with a lower scope. This should be usable by GitHub Actions. --- Library/Homebrew/dev-cmd/update-sponsors.rb | 31 +++---- Library/Homebrew/test/utils/github_spec.rb | 8 -- Library/Homebrew/utils/github.rb | 94 ++++++++++++--------- Library/Homebrew/utils/github/api.rb | 16 ++-- README.md | 4 +- 5 files changed, 80 insertions(+), 73 deletions(-) diff --git a/Library/Homebrew/dev-cmd/update-sponsors.rb b/Library/Homebrew/dev-cmd/update-sponsors.rb index a831d96287..52881fb944 100644 --- a/Library/Homebrew/dev-cmd/update-sponsors.rb +++ b/Library/Homebrew/dev-cmd/update-sponsors.rb @@ -9,8 +9,8 @@ module Homebrew module_function - NAMED_TIER_AMOUNT = 100 - URL_TIER_AMOUNT = 1000 + NAMED_MONTHLY_AMOUNT = 100 + URL_MONTHLY_AMOUNT = 1000 sig { returns(CLI::Parser) } def update_sponsors_args @@ -23,16 +23,16 @@ module Homebrew end end - def sponsor_name(s) - s["name"] || s["login"] + def sponsor_name(sponsor) + sponsor[:name] || sponsor[:login] end - def sponsor_logo(s) - "https://github.com/#{s["login"]}.png?size=64" + def sponsor_logo(sponsor) + "https://github.com/#{sponsor[:login]}.png?size=64" end - def sponsor_url(s) - "https://github.com/#{s["login"]}" + def sponsor_url(sponsor) + "https://github.com/#{sponsor[:login]}" end def update_sponsors @@ -41,18 +41,13 @@ module Homebrew named_sponsors = [] logo_sponsors = [] - GitHub.sponsors_by_tier("Homebrew").each do |tier| - if tier["tier"] >= NAMED_TIER_AMOUNT - named_sponsors += tier["sponsors"].map do |s| - "[#{sponsor_name(s)}](#{sponsor_url(s)})" - end - end + GitHub.sponsorships("Homebrew").each do |s| + largest_monthly_amount = [s[:monthly_amount], s[:closest_tier_monthly_amount]].compact.max + named_sponsors << "[#{sponsor_name(s)}](#{sponsor_url(s)})" if largest_monthly_amount >= NAMED_MONTHLY_AMOUNT - next if tier["tier"] < URL_TIER_AMOUNT + next if largest_monthly_amount < URL_MONTHLY_AMOUNT - logo_sponsors += tier["sponsors"].map do |s| - "[![#{sponsor_name(s)}](#{sponsor_logo(s)})](#{sponsor_url(s)})" - end + logo_sponsors << "[![#{sponsor_name(s)}](#{sponsor_logo(s)})](#{sponsor_url(s)})" end named_sponsors << "many other users and organisations via [GitHub Sponsors](https://github.com/sponsors/Homebrew)" diff --git a/Library/Homebrew/test/utils/github_spec.rb b/Library/Homebrew/test/utils/github_spec.rb index daecb0f52a..da72e7e90c 100644 --- a/Library/Homebrew/test/utils/github_spec.rb +++ b/Library/Homebrew/test/utils/github_spec.rb @@ -57,14 +57,6 @@ describe GitHub do end end - describe "::sponsors_by_tier", :needs_network do - it "errors on an unauthenticated token" do - expect { - described_class.sponsors_by_tier("Homebrew") - }.to raise_error(/INSUFFICIENT_SCOPES|FORBIDDEN|token needs the 'admin:org' scope/) - end - end - describe "::get_artifact_url", :needs_network do it "fails to find a nonexistent workflow" do expect { diff --git a/Library/Homebrew/utils/github.rb b/Library/Homebrew/utils/github.rb index 5253b875b6..c97f92db2e 100644 --- a/Library/Homebrew/utils/github.rb +++ b/Library/Homebrew/utils/github.rb @@ -403,59 +403,75 @@ module GitHub result["organization"]["team"]["members"]["nodes"].to_h { |member| [member["login"], member["name"]] } end - def sponsors_by_tier(user) - query = <<~EOS - { organization(login: "#{user}") { - sponsorsListing { - tiers(first: 10, orderBy: {field: MONTHLY_PRICE_IN_CENTS, direction: DESC}) { + def sponsorships(user) + has_next_page = true + after = "" + sponsorships = [] + errors = [] + while has_next_page + query = <<~EOS + { organization(login: "#{user}") { + sponsorshipsAsMaintainer(first: 100 #{after}) { + pageInfo { + startCursor + hasNextPage + endCursor + } + totalCount nodes { - monthlyPriceInDollars - adminInfo { - sponsorships(first: 100, includePrivate: true) { - totalCount - nodes { - privacyLevel - sponsorEntity { - __typename - ... on Organization { login name } - ... on User { login name } - } - } + tier { + monthlyPriceInDollars + closestLesserValueTier { + monthlyPriceInDollars } } + sponsorEntity { + __typename + ... on Organization { login name } + ... on User { login name } + } } } } } - } - EOS - result = API.open_graphql(query, scopes: ["admin:org", "user"]) + EOS + # Some organisations do not permit themselves to be queried through the + # API like this and raise an error so handle these errors later. + # This has been reported to GitHub. + result = API.open_graphql(query, scopes: ["user"], raise_errors: false) + errors += result["errors"] if result["errors"].present? - tiers = result["organization"]["sponsorsListing"]["tiers"]["nodes"] + current_sponsorships = result["data"]["organization"]["sponsorshipsAsMaintainer"] - tiers.map do |t| - tier = t["monthlyPriceInDollars"] - raise API::Error, "Your token needs the 'admin:org' scope to access this API" if t["adminInfo"].nil? + # The organisations mentioned above will show up as nil nodes. + sponsorships += current_sponsorships["nodes"].compact - sponsorships = t["adminInfo"]["sponsorships"] - count = sponsorships["totalCount"] - sponsors = sponsorships["nodes"].map do |sponsor| - next unless sponsor["privacyLevel"] == "PUBLIC" + if (page_info = current_sponsorships["pageInfo"].presence) && + page_info["hasNextPage"].presence + after = %Q(, after: "#{page_info["endCursor"]}") + else + has_next_page = false + end + end - se = sponsor["sponsorEntity"] - { - "name" => se["name"].presence || sponsor["login"], - "login" => se["login"], - "type" => se["__typename"].downcase, - } - end.compact + # Only raise errors if we didn't get any sponsorships. + if sponsorships.blank? && errors.present? + raise API::Error, errors.map { |e| "#{e["type"]}: #{e["message"]}" }.join("\n") + end + + sponsorships.map do |sponsorship| + closest_tier_monthly_amount = sponsorship["tier"].fetch("closestLesserValueTier", nil) + &.fetch("monthlyPriceInDollars", nil) + monthly_amount = sponsorship["tier"]["monthlyPriceInDollars"] + sponsor = sponsorship["sponsorEntity"] { - "tier" => tier, - "count" => count, - "sponsors" => sponsors, + name: sponsor["name"].presence || sponsor["login"], + login: sponsor["login"], + monthly_amount: monthly_amount, + closest_tier_monthly_amount: closest_tier_monthly_amount || 0, } - end.compact + end end def get_repo_license(user, repo) diff --git a/Library/Homebrew/utils/github/api.rb b/Library/Homebrew/utils/github/api.rb index 005f5baf7b..b802a4af24 100644 --- a/Library/Homebrew/utils/github/api.rb +++ b/Library/Homebrew/utils/github/api.rb @@ -252,17 +252,19 @@ module GitHub end end - def open_graphql(query, variables: nil, scopes: [].freeze) + def open_graphql(query, variables: nil, scopes: [].freeze, raise_errors: true) data = { query: query, variables: variables } result = open_rest("#{API_URL}/graphql", scopes: scopes, data: data, request_method: "POST") - if result["errors"].present? - raise Error, result["errors"].map { |e| - "#{e["type"]}: #{e["message"]}" - }.join("\n") - end + if raise_errors + if result["errors"].present? + raise Error, result["errors"].map { |e| "#{e["type"]}: #{e["message"]}" }.join("\n") + end - result["data"] + result["data"] + else + result + end end def raise_error(output, errors, http_code, headers, scopes) diff --git a/README.md b/README.md index d4933da4a4..8631f38118 100644 --- a/README.md +++ b/README.md @@ -85,4 +85,6 @@ Flaky test detection and tracking is provided by [BuildPulse](https://buildpulse [![DNSimple](https://cdn.dnsimple.com/assets/resolving-with-us/logo-light.png)](https://dnsimple.com/resolving/homebrew#gh-light-mode-only) [![DNSimple](https://cdn.dnsimple.com/assets/resolving-with-us/logo-dark.png)](https://dnsimple.com/resolving/homebrew#gh-dark-mode-only) -Homebrew is generously supported by [Randy Reddig](https://github.com/ydnar), [Codecademy](https://github.com/Codecademy), [Appwrite](https://github.com/appwrite), [embark-studios](https://github.com/embark-studios) and many other users and organisations via [GitHub Sponsors](https://github.com/sponsors/Homebrew). +Homebrew is generously supported by [GitHub](https://github.com/github), [Custom Ink](https://github.com/customink), [Randy Reddig](https://github.com/ydnar), [Christian (Xian) M. Lilley](https://github.com/xml), [David Fernandez](https://github.com/lidstromberg), [embark-studios](https://github.com/embark-studios) and many other users and organisations via [GitHub Sponsors](https://github.com/sponsors/Homebrew). + +[![GitHub](https://github.com/github.png?size=64)](https://github.com/github)