はじめに
Qiita 社では Slack bot に Ruboty を Qiitan として利用しています。
Qiitan (Ruboty) に GitHub の issue/pull request を検索させるというニーズが出てきたため、 "ruboty-qiita-github" に search issues
command をさっと追加しました
今後 Qiitan (Ruboty) に新しい command を追加する時が来たら参考にしてください
GitHub の issue/pull request を検索する command を追加する
GitHub API を確認する
まず、 GitHub の issue/pull request を検索する Interface を確認します
GitHub REST API については、以下 GitHub Docs から探しましょう
今回必要な API は Search issues and pull requests です
必要な parameters や query parameters すべて言及することは難しいため 以下 docs を参照してください
Search issues and pull requests | Search - GitHub Docs
また、 query parameters q
については、以下 docs を参照することをおすすめです
Filtering and searching issues and pull requests - GitHub Docs
GitHub REST API に記載されている API は GitHub CLI で簡単に確認できるため確認してみましょう
今回は、 usrt:mziyut
(@mziyut がもつ repository) の is:open
(issue/pull request が Open されている) の is:public
(public な repository) の issue/pull request を 1 件取得するという条件で issue/pull request を検索しました
% gh api -H "Accept: application/vnd.github+json" '/search/issues?q=user:mziyut+is:public+is:open&per_page=1'
実行結果は以下のようになります
Click me
{
"total_count": 31,
"incomplete_results": false,
"items": [
{
"url": "https://api.github.com/repos/mziyut/honkit-plugin-prism/issues/7",
"repository_url": "https://api.github.com/repos/mziyut/honkit-plugin-prism",
"labels_url": "https://api.github.com/repos/mziyut/honkit-plugin-prism/issues/7/labels{/name}",
"comments_url": "https://api.github.com/repos/mziyut/honkit-plugin-prism/issues/7/comments",
"events_url": "https://api.github.com/repos/mziyut/honkit-plugin-prism/issues/7/events",
"html_url": "https://github.com/mziyut/honkit-plugin-prism/pull/7",
"id": 1410719907,
"node_id": "PR_kwDOIJ_6M85A4onc",
"number": 7,
"title": "Bump @types/node from 18.8.3 to 18.11.0",
"user": {
"login": "dependabot[bot]",
"id": 49699333,
"node_id": "MDM6Qm90NDk2OTkzMzM=",
"avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/dependabot%5Bbot%5D",
"html_url": "https://github.com/apps/dependabot",
"followers_url": "https://api.github.com/users/dependabot%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/dependabot%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/dependabot%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/dependabot%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/dependabot%5Bbot%5D/received_events",
"type": "Bot",
"site_admin": false
},
"labels": [
{
"id": 4678247549,
"node_id": "LA_kwDOIJ_6M88AAAABFthkfQ",
"url": "https://api.github.com/repos/mziyut/honkit-plugin-prism/labels/dependencies",
"name": "dependencies",
"color": "0366d6",
"default": false,
"description": "Pull requests that update a dependency file"
},
{
"id": 4678247557,
"node_id": "LA_kwDOIJ_6M88AAAABFthkhQ",
"url": "https://api.github.com/repos/mziyut/honkit-plugin-prism/labels/javascript",
"name": "javascript",
"color": "168700",
"default": false,
"description": "Pull requests that update Javascript code"
}
],
"state": "open",
"locked": false,
"assignee": null,
"assignees": [],
"milestone": null,
"comments": 0,
"created_at": "2022-10-17T01:17:11Z",
"updated_at": "2022-10-17T01:17:13Z",
"closed_at": null,
"author_association": "NONE",
"active_lock_reason": null,
"draft": false,
"pull_request": {
"url": "https://api.github.com/repos/mziyut/honkit-plugin-prism/pulls/7",
"html_url": "https://github.com/mziyut/honkit-plugin-prism/pull/7",
"diff_url": "https://github.com/mziyut/honkit-plugin-prism/pull/7.diff",
"patch_url": "https://github.com/mziyut/honkit-plugin-prism/pull/7.patch",
"merged_at": null
},
"body": "Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.8.3 to 18.11.0.\n<details>\n<summary>Commits</summary>\n<ul>\n<li>See full diff in <a href=\"https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node\">compare view</a></li>\n</ul>\n</details>\n<br />\n\n\n[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/node&package-manager=npm_and_yarn&previous-version=18.8.3&new-version=18.11.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)\n\nDependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.\n\n[//]: # (dependabot-automerge-start)\n[//]: # (dependabot-automerge-end)\n\n---\n\n<details>\n<summary>Dependabot commands and options</summary>\n<br />\n\nYou can trigger Dependabot actions by commenting on this PR:\n- `@dependabot rebase` will rebase this PR\n- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it\n- `@dependabot merge` will merge this PR after your CI passes on it\n- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it\n- `@dependabot cancel merge` will cancel a previously requested merge and block automerging\n- `@dependabot reopen` will reopen this PR if it is closed\n- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually\n- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)\n- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)\n\n\n</details>",
"reactions": {
"url": "https://api.github.com/repos/mziyut/honkit-plugin-prism/issues/7/reactions",
"total_count": 0,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 0,
"rocket": 0,
"eyes": 0
},
"timeline_url": "https://api.github.com/repos/mziyut/honkit-plugin-prism/issues/7/timeline",
"performed_via_github_app": null,
"state_reason": null,
"score": 1.0
}
]
}
今回、 Issue/Pull request の検索に必要な API が定義され、返却される項目が理解できたら次のステップに進みましょう
Gem octkit
を確認する
ruboty-qiita-github
では octokit/octokit.rb
を利用し GitHub API 経由でデータを取得・操作しています
# Search issues
#
# @param query [String] Search term and qualifiers
# @param options [Hash] Sort and pagination options
# @option options [String] :sort Sort field
# @option options [String] :order Sort order (asc or desc)
# @option options [Integer] :page Page of paginated results
# @option options [Integer] :per_page Number of items per page
# @return [Sawyer::Resource] Search results object
# @see https://developer.github.com/v3/search/#search-issues-and-pull-requests
def search_issues(query, options = {})
search 'search/issues', query, options
end
先ほど GitHub REST API で確認した endpoint を叩けることが確認できました。
ruboty-qiita-github
に command を追加する
必要な GitHub API が確認できたら ruboty-qiita-github
に command を定義します
command を定義するために以下ファイルを変更、作成しましょう
- 新規作成
lib/ruboty/github/actions/search_issues.rb
- 変更
lib/ruboty/handlers/github.rb
lib/ruboty/github.rb
実際のコードを例に役割など説明していきます
lib/ruboty/github/actions/search_issues.rb
まず先に、 search issues
の処理を定義しましょう
lib/ruboty/github/actions/search_issues.rb
を新規作成します
処理の流れとしては...
- GitHub Token が登録されているか確認
- 登録されていない場合、エラーを返却
- octkit (code 上は client) 経由で search_issues API からデータを取得する
- 結果を整形して、返却する
# frozen_string_literal: true
module Ruboty
module Github
module Actions
class SearchIssues < Base
def call
if has_access_token?
search
else
require_access_token
end
end
private
def search
results = search_issues.items.each_with_object(["Searched: '#{query}'"]) do |item, object|
repository_url = item[:repository_url].delete_prefix('https://api.github.com/repos/')
object << "[#{repository_url}##{item[:number]}] #{item[:title]} (#{item[:user][:login]})\n#{item[:html_url]}"
end
message.reply(results.join("\n"))
rescue Octokit::Unauthorized
message.reply('Failed in authentication (401)')
rescue Octokit::NotFound
message.reply('Could not find that repository')
rescue StandardError => e
message.reply("Failed by #{e.class}")
end
def query
message[:query]
end
def search_issues
client.search_issues(query)
end
end
end
end
end
処理が定義できたら次のステップに進みましょう
lib/ruboty/github.rb
先ほど作成した lib/ruboty/github/actions/search_issues.rb
を require します
require 'ruboty/github/actions/search_issues'
lib/ruboty/handlers/github.rb
lib/ruboty/handlers/github.rb
は Ruboty が受け取ったコマンドを定義された process に dispatch する役割を担っています
今回追加した処理は以下 2 つです
on(
/search issues "(?<query>.+)"/, # 追加する command の条件を記載
name: "search_issues", # dispatch する method 名を記載
description: "Search an issue", # command の説明を記載
)
def search_issues(message)
Ruboty::Github::Actions::SearchIssues.new(message).call
end
ここまでで、 Ruboty に対して search issues "user:foo"
等と送信すると指定した処理が実行されるようになります
その他
必要に応じて今回追加した command に対するテストも書きましょう
spec/ruboty/handlers/github_spec.rb
最後に
今回は Ruboty の plugin である ruboty-qiita-github
に簡単な command を追加してみました
コマンドを追加するだけであれば簡単なので Ruboty の plugin の開発に try してみてはいかがでしょうか
Reference