From bc295f79476e1218701fe5c2b8d39a8a9cf74842 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Wed, 7 Sep 2022 16:49:41 +0100 Subject: [PATCH 1/2] 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 a831d96287f41..52881fb944063 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 @@ def update_sponsors_args 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 @@ def update_sponsors 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 daecb0f52a66a..da72e7e90cef2 100644 --- a/Library/Homebrew/test/utils/github_spec.rb +++ b/Library/Homebrew/test/utils/github_spec.rb @@ -57,14 +57,6 @@ 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 5253b875b6615..c97f92db2e933 100644 --- a/Library/Homebrew/utils/github.rb +++ b/Library/Homebrew/utils/github.rb @@ -403,59 +403,75 @@ def members_by_team(org, team) 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 005f5baf7bdef..b802a4af240db 100644 --- a/Library/Homebrew/utils/github/api.rb +++ b/Library/Homebrew/utils/github/api.rb @@ -252,17 +252,19 @@ def paginate_rest(url, per_page: 100) 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 d4933da4a4e7e..8631f381184ed 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) From 8be90946228f742ad7f6c616f1c84935887cbf80 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Thu, 8 Sep 2022 10:45:15 +0100 Subject: [PATCH 2/2] sponsors-maintainers-man-completions: update sponsors too. --- ...> sponsors-maintainers-man-completions.yml} | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) rename .github/workflows/{maintainers-man-completions.yml => sponsors-maintainers-man-completions.yml} (79%) diff --git a/.github/workflows/maintainers-man-completions.yml b/.github/workflows/sponsors-maintainers-man-completions.yml similarity index 79% rename from .github/workflows/maintainers-man-completions.yml rename to .github/workflows/sponsors-maintainers-man-completions.yml index 893f4dcb27ebc..778cce8e7a427 100644 --- a/.github/workflows/maintainers-man-completions.yml +++ b/.github/workflows/sponsors-maintainers-man-completions.yml @@ -1,9 +1,9 @@ -name: Update maintainers, manpage and completions +name: Update sponsors, maintainers, manpage and completions on: push: paths: - - .github/workflows/maintainers-man-completions.yml + - .github/workflows/sponsors-maintainers-man-completions.yml - README.md - Library/Homebrew/cmd/** - Library/Homebrew/dev-cmd/** @@ -54,7 +54,7 @@ jobs: then BRANCH="$GITHUB_REF_NAME" else - BRANCH=maintainers-man-completions + BRANCH=sponsors-maintainers-man-completions fi echo "::set-output name=branch::${BRANCH}" @@ -69,13 +69,21 @@ jobs: git checkout --no-track -B "${BRANCH}" origin/master fi + if brew sponsors + then + git add "${GITHUB_WORKSPACE}/README.md" + git commit -m "Update sponsors." \ + -m "Autogenerated by the [sponsors-maintainers-man-completions](https://github.com/Homebrew/brew/blob/HEAD/.github/workflows/sponsors-maintainers-man-completions.yml) workflow." + COMMITTED=true + fi + if brew update-maintainers then git add "${GITHUB_WORKSPACE}/README.md" \ "${GITHUB_WORKSPACE}/docs/Manpage.md" \ "${GITHUB_WORKSPACE}/manpages/brew.1" git commit -m "Update maintainers." \ - -m "Autogenerated by the [update-man-completions](https://github.com/Homebrew/brew/blob/HEAD/.github/workflows/maintainers-man-completions.yml) workflow." + -m "Autogenerated by the [sponsors-maintainers-man-completions](https://github.com/Homebrew/brew/blob/HEAD/.github/workflows/sponsors-maintainers-man-completions.yml) workflow." COMMITTED=true fi @@ -86,7 +94,7 @@ jobs: "${GITHUB_WORKSPACE}/manpages/brew.1" \ "${GITHUB_WORKSPACE}/completions" git commit -m "Update manpage and completions." \ - -m "Autogenerated by the [update-man-completions](https://github.com/Homebrew/brew/blob/HEAD/.github/workflows/maintainers-man-completions.yml) workflow." + -m "Autogenerated by the [sponsors-maintainers-man-completions](https://github.com/Homebrew/brew/blob/HEAD/.github/workflows/sponsors-maintainers-man-completions.yml) workflow." COMMITTED=true fi