require "http" require "json" require "slack-notifier" require_relative "./shared/git_config" require_relative "./shared/team" module Aid module Scripts class Review < Aid::Script include Aid::GitConfig def self.description "Requests a review from one or more team members" end def self.help <<~HELP Usage: $ aid review Type aid review from your feature branch and follow the prompts HELP end def run check_for_minimum_version_of_hub! prompt_for_reviewers! run_aid_finish_if_needed! change_wip_to_reviewable_label! move_asana_task_to_pull_request_column! step "Marking PR ready for review" do pull_request.mark_ready_for_review! puts "✅ done" end add_reviewers_to_pr! ping_reviewers_on_slack! end private def ping_reviewers_on_slack! step "Pinging reviewers on slack" do slack.ping( slack_message, username: "review-bot", channel: slack_channel ) end end def github_username git_config("github.user") end def current_user_slack_tag member = team.members.find { |member| member.github_username == github_username } member&.slack_tag || git_config("user.name") end def slack_message user_tags = @reviewers.map(&:slack_tag) <<~MSG :pray: *Review request from #{current_user_slack_tag}* _#{pull_request.title}_ #{pull_request.url} Requested: #{user_tags.join(', ')} MSG end def slack @slack ||= Slack::Notifier.new(slack_webhook) end def slack_webhook "https://hooks.slack.com/services/T02A0DJMA/"\ "BN49T3ZC0/Q536v8O5pzn1NSqDJ9BpEV3s" end def slack_channel "#verisure" end def add_reviewers_to_pr! step "Assigning reviewers on Github" do github_client.post( "#{api_prefix}/pulls/#{pull_request.id}/requested_reviewers", json: { reviewers: @reviewers.map(&:github_username) } ) end end def check_for_minimum_version_of_hub! match = `hub --version`.strip.match(/hub version (.+)$/) version = Gem::Version.new(match[1]) min_version = "2.8.4" if version < Gem::Version.new(min_version) abort "aid review requires hub at #{min_version} or greater. "\ "Run `brew upgrade hub` to upgrade." end rescue StandardError abort "Error checking for hub version. Ensure you have it installed with "\ "$ brew install hub" end def run_aid_finish_if_needed! branch_commit_logs = `git log --format=full master..`.strip if branch_commit_logs =~ %r{Finishes:.+https://app.asana.com}mi step "Doing one last push to GitHub" do system! "git push --force-with-lease origin #{current_branch_name}" end else step "Running aid finish..." do Finish.run end end end def move_asana_task_to_pull_request_column! step "Moving Asana task to Pull Request column" do asana_task.add_project( project: asana_project.gid, section: asana_pr_section.gid ) end end def team @team ||= Aid::Team.from_yml("#{project_root}/.team.yml") end def prompt_for_reviewers! puts "Which reviewers do you want to review this PR?" puts @reviewers = team.prompt_for_members puts abort colorize(:red, "Please select a reviewer from the list") if @reviewers.empty? end def change_wip_to_reviewable_label! step "Changing WIP to reviewable label" do puts "Deleting WIP label..." github_client.delete("#{api_prefix}/issues/#{pull_request.id}/labels/wip") puts "Adding reviewable label..." github_client.post( "#{api_prefix}/issues/#{pull_request.id}/labels", json: { labels: ["reviewable"] } ) end end def api_prefix "https://api.github.com/repos/#{repo_name}" end def github_auth_token @github_auth_token ||= load_github_auth_token end def load_github_auth_token credential_file = "#{ENV['HOME']}/.config/hub" abort "No hub credentials in #{credential_file}" unless File.exist?(credential_file) credentials = YAML.safe_load(File.read(credential_file)) credentials["github.com"][0]["oauth_token"] end def github_client HTTP.headers("Authorization" => "token #{github_auth_token}") end def asana_task_id @asana_task_id ||= begin result = current_branch_name.strip.match(/\d+$/) result && result[0] end end def current_branch_name `git rev-parse --abbrev-ref HEAD`.strip end def repo_name remote_url = `git remote get-url origin`.strip result = remote_url.match(%r{github.com[:/](?\w+/\w+)(?:\.git)?}) result && result[:repo_name] end def pull_request @pull_request ||= local_github_pull_request || find_remote_github_pull_request end def local_github_pull_request id = git_config("asana.#{asana_task_id}.pull-request-id") title = git_config("asana.#{asana_task_id}.name") return nil unless id && title PullRequest.new( id: id, title: title, repo_name: repo_name ) end def find_remote_github_pull_request sep = "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=" cmd = %(hub pr list --format="#PR: %i\nTitle: %t\n\n%b\n#{sep}\n") raw_pull_requests = `#{cmd}`.strip pull_requests = raw_pull_requests.split(sep) request = pull_requests.detect { |pr| pr.include?(asana_task_id) } abort "Could not find a PR that matches story #{asana_task_id}" unless request id = request.match(/PR: #(\d+)/)[1] title = request.match(/Title: (.+?)\n/)[1] PullRequest.new( id: id, title: title, repo_name: repo_name ) end def asana_task @asana_task ||= asana.tasks.find_by_id(asana_task_id) end def asana_project @asana_project ||= asana.projects.find_by_id(asana_project_id) end def asana_project_id git_config("asana.project-id").to_i end def asana_sections asana.sections.find_by_project(project: asana_project.gid) end def asana_pr_section asana_sections.detect do |task| task.name =~ /Pull Request/i end end def asana @asana ||= Asana::Client.new do |client| client.default_headers "asana-enable" => "string_ids,new_sections" client.authentication :access_token, git_config("asana.personal-access-token") end end class PullRequest attr_reader :id, :title, :url def initialize(id:, title:, repo_name:) @id = id @title = title @url = "https://github.com/#{repo_name}/pull/#{id}" end # rubocop:disable Metrics/MethodLength def mark_ready_for_review! cmd = <<~CMD hub api graphql \ -H "Accept: application/vnd.github.shadow-cat-preview+json" \ -f query=' mutation MarkPullRequestReady { markPullRequestReadyForReview( input: { pullRequestId:"#{graphql_id}" } ) { pullRequest { isDraft number } } } ' CMD `#{cmd}` end private def graphql_id @graphql_id ||= find_graphql_id end def find_graphql_id cmd = <<~CMD hub api graphql -f query=' query FindPullRequestId { repository(owner:"abtion", name:"verisure") { pullRequests(states:OPEN, first: 25) { nodes { id number } } } } ' CMD json = `#{cmd}`.strip response = JSON.parse(json) pull_requests = response.dig( "data", "repository", "pullRequests", "nodes" ) request = pull_requests.find { |pr| pr["number"].to_s == id.to_s } request["id"] end end end end end