summaryrefslogtreecommitdiff
path: root/tool/auto_review_pr.rb
blob: 07c98c7e0aed9efd285ca7dfddfa77388d72d264 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'json'
require 'net/http'
require 'uri'
require_relative './sync_default_gems'

class GitHubAPIClient
  def initialize(token)
    @token = token
  end

  def get(path)
    response = Net::HTTP.get_response(URI("https://api.github.com#{path}"), {
      'Authorization' => "token #{@token}",
      'Accept' => 'application/vnd.github.v3+json',
    }).tap(&:value)
    JSON.parse(response.body, symbolize_names: true)
  end

  def post(path, body = {})
    body = JSON.dump(body)
    response = Net::HTTP.post(URI("https://api.github.com#{path}"), body, {
      'Authorization' => "token #{@token}",
      'Accept' => 'application/vnd.github.v3+json',
      'Content-Type' => 'application/json',
    }).tap(&:value)
    JSON.parse(response.body, symbolize_names: true)
  end
end

class AutoReviewPR
  REPO = 'ruby/ruby'

  COMMENT_USER = 'github-actions[bot]'
  COMMENT_PREFIX = 'The following files are maintained in the following upstream repositories:'
  COMMENT_SUFFIX = 'Please file a pull request to the above instead. Thank you!'

  def initialize(client)
    @client = client
  end

  def review(pr_number)
    # Fetch the list of files changed by the PR
    changed_files = @client.get("/repos/#{REPO}/pulls/#{pr_number}/files").map { it.fetch(:filename) }

    # Build a Hash: { upstream_repo => files, ... }
    upstream_repos = SyncDefaultGems::Repository.group(changed_files)
    upstream_repos.delete(nil) # exclude no-upstream files
    upstream_repos.delete('prism') if changed_files.include?('prism_compile.c') # allow prism changes in this case
    if upstream_repos.empty?
      puts "Skipped: The PR ##{pr_number} doesn't have upstream repositories."
      return
    end

    # Check if the PR is already reviewed
    existing_comments = @client.get("/repos/#{REPO}/issues/#{pr_number}/comments")
    existing_comments.map! { [it.fetch(:user).fetch(:login), it.fetch(:body)] }
    if existing_comments.any? { |user, comment| user == COMMENT_USER && comment.start_with?(COMMENT_PREFIX) }
      puts "Skipped: The PR ##{pr_number} already has an automated review comment."
      return
    end

    # Post a comment
    comment = format_comment(upstream_repos)
    result = @client.post("/repos/#{REPO}/issues/#{pr_number}/comments", { body: comment })
    puts "Success: #{JSON.pretty_generate(result)}"
  end

  private

  # upstream_repos: { upstream_repo => files, ... }
  def format_comment(upstream_repos)
    comment = +''
    comment << "#{COMMENT_PREFIX}\n\n"

    upstream_repos.each do |upstream_repo, files|
      comment << "* https://github.com/ruby/#{upstream_repo}\n"
      files.each do |file|
        comment << "    * #{file}\n"
      end
    end

    comment << "\n#{COMMENT_SUFFIX}"
    comment
  end
end

pr_number = ARGV[0] || abort("Usage: #{$0} <pr_number>")
client = GitHubAPIClient.new(ENV.fetch('GITHUB_TOKEN'))

AutoReviewPR.new(client).review(pr_number)