Skip to content

Commit

Permalink
Add workflow script for repository management
Browse files Browse the repository at this point in the history
  • Loading branch information
pocke committed Feb 20, 2024
1 parent 01361bb commit 03b716d
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 0 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/check_review_status.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: 'Check review status'

on:
pull_request: {}

jobs:
check_review_status:
runs-on: 'ubuntu-latest'
permissions:
pull-requests: read
contents: read
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
with:
filter: 'blob:none'
fetch-depth: 0
ref: main
- name: Changed gems and non gem files
id: changes
run: |
ruby .github/workflows/pr_bot/changed_files.rb
- name: Check review status
env:
CHANGED_GEMS: ${{ steps.changes.outputs.gems }}
CHANGED_NON_GEMS: ${{ steps.changes.outputs.non_gems }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ruby .github/workflows/pr_bot/check_review_status.rb "$CHANGED_GEMS" "$CHANGED_NON_GEMS" "$PR_NUMBER"
55 changes: 55 additions & 0 deletions .github/workflows/merge_on_comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: 'Merge a PR on a comment'

on:
issue_comment:
types: [created]

jobs:
merge:
if: ${{ github.event.issue.pull_request && github.event.comment.body == '/merge' }}
runs-on: 'ubuntu-latest'
permissions:
pull-requests: write
contents: write
steps:
- uses: actions/checkout@v4
with:
filter: 'blob:none'
fetch-depth: 0
ref: main
- name: Fetch PR information
id: pr_info
env:
PR_NUMBER: ${{ github.event.issue.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
pr="$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/${GH_REPO}/pulls/${PR_NUMBER})"
author="$(echo "$pr" | jq -r .user.login)"
head_sha="$(echo "$pr" | jq -r .head.sha)"
base_sha="$(echo "$pr" | jq -r .base.sha)"
echo "author=$author" >> $GITHUB_OUTPUT
echo "head_sha=$head_sha" >> $GITHUB_OUTPUT
echo "base_sha=$base_sha" >> $GITHUB_OUTPUT
- name: Changed gems and non gem files
env:
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
id: changes
run: |
ruby .github/workflows/pr_bot/changed_files.rb
- name: Merge the PR
env:
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
PR_AUTHOR: ${{ steps.pr_info.outputs.author }}
CHANGED_GEMS: ${{ steps.changes.outputs.gems }}

COMMENTED_BY: ${{ github.event.issue.user.login }}
PR_NUMBER: ${{ github.event.issue.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
ruby .github/workflows/pr_bot/check_merge_ability.rb "$CHANGED_GEMS"
gh pr merge "$PR_NUMBER" --repo "$GH_REPO" --merge
8 changes: 8 additions & 0 deletions .github/workflows/pr_bot/changed_files.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require_relative "./utils"

paths = `git diff --name-only -z #{BASE_SHA} #{HEAD_SHA}`.split("\0")
changed_gems = paths.select { _1.start_with?("gems/") }.map { _1.split("/")[1] }.uniq
changed_non_gems = paths.reject { _1.start_with?("gems/") }

output :gems, JSON.generate(changed_gems)
output :non_gems, JSON.generate(changed_non_gems)
17 changes: 17 additions & 0 deletions .github/workflows/pr_bot/check_merge_ability.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require_relative "./utils"

def can_merge?(commented_by, author, gem_reviewers)
return true if commented_by == author
return true if administorators.include?(commented_by)
return true if gem_reviewers.include?(commented_by)
return false
end

def all_gem_reviewers(changed_gems)
changed_gems.flat_map { |gem| gem_reviewers(gem, BASE_SHA) }
end

reviewers = all_gem_reviewers(JSON.parse(ARGV[0]))
return if can_merge?(ENV['COMMENTED_BY'], PR_AUTHOR, reviewers)

raise "You do not have permission to merge this PR."
49 changes: 49 additions & 0 deletions .github/workflows/pr_bot/check_review_status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require_relative "./utils"
require "json"

def gem_accepted?(gem, approvers)
reviewers = gem_reviewers(gem, BASE_SHA)

# If reviewers is empty, it means that anyone cannot approve this PR.
# So, we can merge this PR without approval.
return true if reviewers.empty?

# If the author is a reviewer, they can merge this PR themselves.
return true if reviewers.include?(PR_AUTHOR)

not (reviewers & approvers).empty?
end

def non_gem_accepted?(approvers)
admins = administorators.map { _1['login'] }

not (approvers & admins).empty?
end

def main(changed_gems, changed_non_gems, pr_number)
approvers = approvements(HEAD_SHA, pr_number).map { _1['user']['login'] }

status = 0

# Check gem files
not_approved_gems = changed_gems.reject { |gem| gem_accepted?(gem, approvers) }
unless not_approved_gems.empty?
puts "The following gems are not approved yet:"
puts not_approved_gems.join("\n")
status = 1
end

# Check non gem files
if !changed_non_gems.empty? && !non_gem_accepted?(approvers)
puts "The following files are changed, but not approved by the admin yet:"
puts changed_non_gems.join("\n")
status = 1
end

exit status
end

changed_gems = JSON.parse(ARGV[0])
changed_non_gems = JSON.parse(ARGV[1])
pr_number = ARGV[2]
main(changed_gems, changed_non_gems, pr_number)
72 changes: 72 additions & 0 deletions .github/workflows/pr_bot/utils.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require 'securerandom'
require 'open3'
require 'yaml'
require "json"

BASE_SHA = ENV['BASE_SHA']
HEAD_SHA = ENV['HEAD_SHA']
PR_AUTHOR = ENV['PR_AUTHOR']
GH_REPO = ENV['GH_REPO']

def output(key, value)
File.open(ENV['GITHUB_OUTPUT'], 'a') do |f|
delimiter = SecureRandom.hex(20)
f.puts "#{key}<<#{delimiter}"
f.puts value
f.puts delimiter
end
puts "Set #{key}=#{value}"
end

class CommandError < StandardError; end

def sh!(*cmd, **opt)
Open3.capture3(*cmd, **opt).then do |out, err, status|
raise CommandError, "Unexpected status #{status.exitstatus}\n\n#{err}" unless status.success?

out
end
end

def git(*cmd, **opt)
sh! 'git', *cmd, **opt
end

def git?(*cmd, **opt)
git(*cmd, **opt)
rescue CommandError
nil
end

def gem_reviewers(gem, sha)
begin
yaml = git 'cat-file', '-p', "#{sha}:gems/#{gem}/_reviewers.yaml"
rescue CommandError
return []
end

content = YAML.safe_load(yaml)
content['reviewers']
end

def log(msg)
puts msg
end

def gh_api!(*args)
resp = sh! 'gh', 'api',
'-H', "Accept: application/vnd.github+json",
'-H' 'X-GitHub-Api-Version: 2022-11-28', *args

JSON.parse(resp)
end

def approvements(sha, pr_number)
reviews = gh_api! "/repos/#{GH_REPO}/pulls/#{pr_number}/reviews"
reviews.select { _1['state'] == 'APPROVED' && _1['commit_id'] == sha }
end

def administorators
users = gh_api! "/repos/#{GH_REPO}/collaborators"
users.select { _1['permissions']['admin'] }
end
97 changes: 97 additions & 0 deletions .github/workflows/pr_bot/welcome_comment_body.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
require_relative "./utils"

ADMINS = %w[@pocke]

changed_gems = JSON.parse(ARGV[0])
changed_non_gems = JSON.parse(ARGV[1])

msgs = [<<~MSG]
@#{PR_AUTHOR} Thanks for your contribution!
Please follow the instructions below for each change.
MSG

# TODO: when the reviewer includes the author

changed_gems.each do |gem|
exist_in_base = git? 'cat-file', '-e', "#{BASE_SHA}:gems/#{gem}"
exist_in_head = git? 'cat-file', '-e', "#{HEAD_SHA}:gems/#{gem}"

msg = "## `#{gem}`\n\n"

case
when !exist_in_base # new gem
msg << <<~MSG
This RBS files are newly added.
You can merge this PR immediately if the CI passes.
Just comment `/merge` to merge this PR.
MSG
when exist_in_head # updated gem
reviewers = gem_reviewers(gem, BASE_SHA)
# TODO: if reviewers do not respond
msg << <<~MSG
You changed RBS files for an existing gem.
MSG

if reviewers.empty?
msg << <<~MSG
This gem does not have reviewers. So you can merge this PR immediately if the CI passes.
We recommend you add yourself to the reviewers for this gem.
# TODO: add a link to the document
MSG
elsif reviewers.include?(PR_AUTHOR)
msg << <<~MSG
You can merge this PR yourself because you are a reviewer of this gem.
Just comment `/merge` to merge this PR.
You can also request a review from other reviewers if you want.
MSG
else
msg << <<~MSG
You need to get approval from the reviewers of this gem.
#{reviewers.map { "@#{_1}" }.join(', ')}, please review and approve the changes.
After that, the PR author or the reviewers can merge this PR.
Just comment `/merge` to merge this PR.
MSG
end
when !exist_in_head # removed gem
reviewers = gem_reviewers(gem, BASE_SHA)

msg << <<~MSG
You removed RBS files for this gem.
MSG

if reviewers.empty?
msg << <<~MSG
You can merge this PR immediately if the CI passes.
Just comment `/merge` to merge this PR.
MSG
else
msg << <<~MSG
You need to get approval from the reviewers of this gem.
#{reviewers.map { "@#{_1}" }.join(', ')}, please review and approve the changes.
After that, the PR author or the reviewers can merge this PR.
Just comment `/merge` to merge this PR.
MSG
end
else
raise "unreachable"
end

msgs << msg
end

unless changed_non_gems.empty?
msgs << <<~MSG
You changed non-gem files.
#{ADMINS.join(', ')}, please review and approve the changes.
MSG
end

body = msgs.join("\n\n-----\n\n")

output :welcome_comment_body, body
43 changes: 43 additions & 0 deletions .github/workflows/welcome_comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: 'Test PR comment'

on:
pull_request:
types: [opened]

jobs:
create_comment:
runs-on: 'ubuntu-latest'
permissions:
pull-requests: write
contents: read
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
with:
filter: 'blob:none'
fetch-depth: 0
ref: main
- name: Changed gems and non gem files
id: changes
run: |
# TODO: checkout the main branch
ruby .github/workflows/pr_bot/changed_files.rb
- name: Prepare comment body
id: comment-body
env:
CHANGED_GEMS: ${{ steps.changes.outputs.gems }}
CHANGED_NON_GEMS: ${{ steps.changes.outputs.non_gems }}
run: |
ruby .github/workflows/pr_bot/welcome_comment_body.rb "$CHANGED_GEMS" "$CHANGED_NON_GEMS"
- name: test message
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
COMMENT_BODY: ${{ steps.comment-body.outputs.welcome_comment_body }}
run:
gh pr comment "$PR_NUMBER" --body "$COMMENT_BODY" --repo "$GH_REPO"

0 comments on commit 03b716d

Please sign in to comment.