これは SmartHR Advent Calendar 2020 - Qiita 7日目の記事です。
日々の開発時に Pull Request を作ったり、またそのレビューをしたり、レビューが終われば リリースしたりすると思います。
その際に、データパッチの実行を忘れないようにしたり、migration が含まれてるリリースになっているかを ひと目でわかりやすくするためのものを改て実装したので紹介です。
TL;DR
- もともと 2017年につくった AWS lambda (nodejs 6.10) の実装があった
- ↑の実行環境が EOL
- ruby で実装し直したので紹介
やりたいこと
- 特定のファイルが更新されたらその旨をラベルにつけたい
- db/migrate/* のファイルが更新されたら db:migrate ラベルを付けたい
- etc
- base branch が特定の条件であればラベルをつけたい
- 大きめの機能開発をする際に features/*** ブランチをきって開発しているので、そのさいに features/any-branch-name のラベルをつけたい
他の方法
実装的な課題と解決
- GitHub REST API は Pull Request にふくまれる change files が300までしか取得できない
- lambda の実装時は、 REST API を利用していたので、稀に対象の PR が大きいものになっていると change files が全て取得できない問題があった
- GitHub GraphQL API では300以上取得できる
- ruby 実装の際に、 change files は GraphQL API、 ラベルをつける実装は REST API の実装のハイブリッド構成にした
- また、社内の複数のレポジトリでも動くようにかつ、それぞれの設定ができるようにする
実行時のフロー
- Pull Request の Webhook を受け取る
- payload の event の状態を見る
- 対象の event であれば、 GraphQL API を call して、 change files を全て取得する
- 設定に応じて、ファイルをチェックし、ラベルのリストを作る
- REST API をつかって、 Pull Request にラベルを設定する
実行フローのサンプル
class Githubs::PrLabelerController < ApplicationController
def create
event = request.env['HTTP_X_GITHUB_EVENT']
unless event == 'pull_request'
msg = { message: 'event is not pull_request.' }
render json: msg
return
end
github_webhook_secret = ENV['GITHUB_WEBHOOK_SECRET']
if github_webhook_secret
http_x_hub_signature = request.env['HTTP_X_HUB_SIGNATURE']
payload_body = request.body.read
signature = GithubWebhookSignature.new(secret: github_webhook_secret,
http_x_hub_signature: http_x_hub_signature,
payload_body: payload_body)
if signature.invalid?
msg = { message: 'webhook signature is invalid.' }
render json: msg
return
end
end
payload = JSON.parse(params['payload'])
if payload['action'] == 'opened'
pr_number = payload['number']
full_repo = payload['repository']['full_name']
owner, repo = full_repo.split('/')
change_files = GithubQueryHelper.call(pr_number: pr_number, owner: owner, repo: repo)
::Rails.logger.info("💡 #{change_files}")
file_labels = config.labels[repo.to_sym][:files]
targets = {}
if file_labels.present?
file_labels.each_key do |label|
regex = Regexp.new(file_labels[label])
targets[label.to_s] = change_files.select do |v|
v unless v.match(regex).nil?
end
end
end
add_labels = targets.select { |_k, v| v.present? }.keys.uniq
base_branch = payload['pull_request']['base']['ref']
branch_labels = config.labels[repo.to_sym][:branch]
if branch_labels.present?
branch_labels.each_key do |label|
regex = Regexp.new(branch_labels[label])
next if base_branch.match(regex).nil?
add_labels << if label.to_s == 'base-branch'
base_branch
else
label.to_s
end
end
end
octokit = Octokit::Client.new(access_token: ENV['GITHUB_ACCESS_TOKEN'])
octokit.add_labels_to_an_issue(full_repo, pr_number, add_labels) if add_labels.present?
msg = { message: 'ok' }
else
msg = { message: 'action is not opened.' }
end
render json: msg
end
end
GraphQL API をつかった change files 取得サンプル
class GithubQueryHelper
attr_reader :pr_number, :owner, :repo, :github_client
def self.call(pr_number:, owner:, repo:)
new(pr_number: pr_number, owner: owner, repo: repo).call
end
def initialize(pr_number:, owner:, repo:)
@pr_number = pr_number
@owner = owner
@repo = repo
@github_client ||= GithubApi::V4::Client.new(ENV['GITHUB_ACCESS_TOKEN'])
end
def call
retrieve_files_from_pr
end
private
def retrieve_files_from_pr
per_page = 100
options = {
pr_number: pr_number,
per_page: per_page,
owner: owner,
repo: repo
}
files = []
total = github_client.graphql(query: init_query(options))
total_count = total['data']['repository']['pullRequest']['files']['totalCount']
end_cursor = total['data']['repository']['pullRequest']['files']['pageInfo']['endCursor']
page = (total_count / per_page.to_f).ceil
files << total['data']['repository']['pullRequest']['files']['edges'][0]['node']['path']
i = 0
while i < page
options[:endCursor] = end_cursor
end_cursor, filelist = exec_query(list_query(options))
files.concat(filelist)
i += 1
end
files
end
def list_query(options)
<<~QUERY
{
repository(owner: "#{options[:owner]}", name: "#{options[:repo]}") {
pullRequest(number: #{options[:pr_number]}) {
files(first: #{options[:per_page]}, after: "#{options[:endCursor]}") {
pageInfo {
endCursor
startCursor
}
edges {
node {
path
}
}
}
}
}
}
QUERY
end
def init_query(options)
<<~TOTAL_QUERY
{
repository(owner: "#{options[:owner]}", name: "#{options[:repo]}") {
pullRequest(number: #{options[:pr_number]}) {
files(first: 1) {
totalCount
pageInfo {
endCursor
startCursor
}
edges {
node {
path
}
}
}
}
}
}
TOTAL_QUERY
end
def exec_query(query)
data = github_client.graphql(query: query)
end_cursor = data['data']['repository']['pullRequest']['files']['pageInfo']['endCursor']
files = []
data['data']['repository']['pullRequest']['files']['edges'].each do |edge|
files << edge['node']['path']
end
[end_cursor, files]
end
end
おわり
おわり