自作CIサーバー構築によるcommit statusアイコンの違いを探る
こんにちは、M-Yamashitaです。
今回の記事は、自作CIサーバー構築を通じて、GitHub commit statusのアイコンの違いについて調査した話です。
GitHub Actionsもしくは3rd Partyの実行中ワークフローをキャンセルした際、GitHubのcommit statusアイコンが、GitHub Actionsかそれ以外かで異なるようです。ちょっとしたことではありますが、結構気になったので調査し、その結果を記事にしました。
注意:
結論から伝えておくと、なぜアイコンが違うのかわかりませんでした。
この記事では、CIサーバー構築や公開された仕様書をもとに、個人の推測で答えを出しています。
あらかじめご了承ください。
この記事で伝えたいこと
- 自作CIサーバー構築手順
- 自作CIサーバー構築・利用から見るcommit statusアイコンの違いの考察
調査のきっかけ
GitHubにCircleCIを連携し、Pull Requestを作ったときのことです。Pull Requestで起動したCircleCIのワークフローの実行を中断したところ、Pull Requestのcommit statusに、失敗アイコンが表示されました。
よくある光景ですが、ふと気になることがありました。
「GitHub Actionsのワークフロー実行を中断したときも同じアイコンだったっけ?」
確認してみると、GitHub Actionsのワークフローを中断したときは、以下のように八角形に"!"を持ったアイコンで表示されていました。
両方の画像を見比べてみると、アイコン以外にも違う部分がいくつかあります。一例として以下のようなものがあります。
- CircleCIでは"All checks have failed"と表示
- GitHub Actionsでは"Some checks were not successful"と表示
3rd PartyのCIツールを使ったときはこうなると言えばそれまでですが、同じキャンセルのはずなのに、なぜ微妙に違う形式にしているのか気になりました。
公式ドキュメントにはcommit statusについてのページがありますが、このような違いについては記載されていませんでした。
これは、自作CIサーバーを作って何が起きているのか把握するしかないと思い、CIサーバーを作り始めました。
自作CIサーバー構築手順
CIサーバー構築手順は、GitHubが公開しています。
この例ではSinatraを用いて、構築しています。大まかな流れは以下のとおりです。
- Sinatraで自作CIサーバーを起動する
- ngrokを使ってローカルサーバーを外部に公開する
- GitHub上でPull Requestなどのイベントの通知先となるWebhookを設定
- Pull Requestイベント発生時commit statusを変更する処理を自作CIサーバーに実装
では、順に見ていきましょう。
Sinatraで自作CIサーバーを起動する
Gemfileにpuma、sinatraを追加し、server.rbを作成します。
require 'sinatra'
require 'json'
post '/event_handler' do
payload = JSON.parse(params[:payload])
"Well, it worked!"
end
これで自作CIサーバーの最初の一歩が整いました。sinatraを起動します。
❯ bundle exec ruby server.rb
To use retry middleware with Faraday v2.0+, install `faraday-retry` gem
== Sinatra (v3.1.0) has taken the stage on 4567 for development with backup from Puma
Puma starting in single mode...
* Puma version: 6.3.1 (ruby 3.1.2-p20) ("Mugi No Toki Itaru")
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 41756
* Listening on http://127.0.0.1:4567
* Listening on http://[::1]:4567
Use Ctrl-C to stop
これでSinatraの起動は完了です。
ngrokを使って、ローカルサーバーを外部に公開する
sinatraが起動したので、ngrokを使ってローカルサーバーを外部に公開します。
❯ ngrok http 4567
Take our ngrok in production survey! https://forms.gle/aXiBFWzEA36DudFn6
Session Status online
Account xxxxx (Plan: Free)
Version 3.3.4
Region Japan (jp)
Latency 20ms
Web Interface http://127.0.0.1:4040
Forwarding https://xxx-xxx-xxx-xxx.ngrok-free.app -> http://localhost:4567
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
ngrokによる公開はここまでです。
GitHub上でPull Requestなどのイベントの通知先となるWebhookを設定
今回は特定のリポジトリに対するWebhookの設定とします。
Webhookの設定方法には、GUIによる操作、もしくはAPIの実行の2通りがあります。
GUIで設定する方法
GitHubで設定方法のドキュメントが公開されています。
これを元に、添付画像のように設定します。
注意ポイントとして、Payload URLには、ngrokで公開したURLに加えてsinatraで設定したパス(event_handler
)を追加する必要があります。
GUIで設定する方法は以上です。
APIで設定する方法
私が取った設定方法はこちらです。せっかくCIサーバーを作るので、webhookもコードで設定してみようと思い、こちらを選択しました。
こちらもドキュメントが公開されています。
APIを叩くためには、GitHubのPersonal Access Token(PAT)が必要です。PAT
は、GitHubの設定画面から作成できます。PAT作成ではrepo
、admin:repo_hook
の2つのスコープを設定します。
PATを作成したら、APIを叩くコードを作ります。curlでもAPIを叩けますが、Rubyを使ってAPIを叩きます。
require 'json'
require "net/http"
uri = URI.parse("https://api.github.com/repos/M-Yamashita01/RailsSample/hooks")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new(uri.request_uri)
req["Accept"] = "application/vnd.github+json"
req["Authorization"] = "Bearer PERSONAL_ACCESS_TOKEN"
req["X-GitHub-Api-Version"] = "2022-11-28"
req.body = {
"name": "web",
"active": true,
"events": [
"pull_request",
"push"
],
"config": {
"url": "https://xxx-xxx-xxx-xxx.ngrok-free.app/event_handler",
"content_type": "form"
}
}.to_json
http.request(req)
このコードを実行し、GitHub上でWebhook設定画面を開いてみます。前述のGUIでの操作と同じような設定内容が設定されています。
お試しでPull Requestを1つ作ってみる
ここまでで、GitHubのPull Requestイベントを受け取る準備が整いました。
実際にPull Requestを作成してみます。すると、Sinatraのログに以下のようなログが出力されます。
❯ bundle exec ruby server.rb
== Sinatra (v3.1.0) has taken the stage on 4567 for development with backup from Puma
Puma starting in single mode...
* Puma version: 6.3.1 (ruby 3.1.2-p20) ("Mugi No Toki Itaru")
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 46428
* Listening on http://127.0.0.1:4567
* Listening on http://[::1]:4567
Use Ctrl-C to stop
Well, it worked!
140.82.115.54 - - [25/Sep/2023:23:43:09 +0900] "POST /event_handler HTTP/1.1" 200 - 0.0171
Pull Requestのイベントを捉え、Well, it worked!
という文字列を返していることがわかります。
Pull Requestイベント発生時commit statusを変更する処理を自作CIサーバーに実装
commit statusの変更は、octokitを使用して行ないます。
Pull Requestイベントを受け取ったときに、commit statusを変更する処理を作成します。この処理では以下3つを順に行なっています。
- イベントを受け取ったときにcommit statusをpendingに変更
- 5秒待機
- commit statusをsuccessに変更
require 'sinatra'
require 'json'
require 'octokit'
before do
token = 'PERSONAL_ACCESS_TOKEN'
@client ||= Octokit::Client.new(access_token: token)
end
post '/event_handler' do
@payload = JSON.parse(params["payload"])
case request.env['HTTP_X_GITHUB_EVENT']
when "pull_request"
if @payload["action"]
process_pull_request(@payload["pull_request"])
end
end
end
helpers do
def process_pull_request(pull_request)
@client.create_status(pull_request['base']['repo']['full_name'], pull_request['head']['sha'], 'pending', context: 'ci-server-test' )
sleep 5 # do busy work...
@client.create_status(pull_request['base']['repo']['full_name'], pull_request['head']['sha'], 'success', context: 'ci-server-test' )
puts "Pull request processed!"
end
end
では、実際にPull Requestを作成してみます。すると、以下のようにcommit statusが赤い丸のpendingとして表示されます。
5秒後、commit statusがチェックアイコンとなったsuccessが表示されます。
以上で自作CIサーバー構築は完了となります。
自作CIサーバー構築から見るGitHub ActionsとCircleCIのcommit statusアイコンの違いの考察
上記のコードにある通り、commit statusはoctokitのcreate_status
メソッドを使用して変更されています。そのため、このメソッドを調査するとアイコンの違いがわかりそうです。
create_status
メソッドの実装は以下のとおりです。
# Create status for a commit
#
# @param repo [Integer, String, Repository, Hash] A GitHub repository
# @param sha [String] The SHA1 for the commit
# @param state [String] The state: pending, success, failure, error
# @option options [String] :context A context to differentiate this status from others
# @option options [String] :target_url A link to more details about this status
# @option options [String] :description A short human-readable description of this status
# @return [Sawyer::Resource] A status
# @see https://developer.github.com/v3/repos/statuses/#create-a-status
def create_status(repo, sha, state, options = {})
options = options.merge(state: state)
post "#{Repository.path repo}/statuses/#{sha}", options
end
@see
にAPIの仕様書が記載されているので、確認します。
stateがcommit statusを表しているので、ここを確認します。
state
string Required
The state of the status.
Can be one of:error
,failure
,pending
,success
stateには4つの状態のみ存在し、cancelという状態は存在しません。ここまでを踏まえて、アイコンの違いを推測してみます。
CircleCIなどの3rd partyツールでは、APIを使用するしかないため、指定のstateしか使用できません。そのため、ワークフローがキャンセルだったとしても、errorもしくはfailureをGitHubに送っているのではないかと考えられます。
一方、GitHub Actionsではワークフローキャンセル時にキャンセル専用のアイコンを表示しています。そのため推測できるGitHubの実装は以下2つのどちらかです。
- 3rd partyツールと同様にAPIを叩いている
- ただし
status
は、キャンセルに相当する隠し値も受け取れるようになっているため、その値を使用している
- ただし
- APIを叩かずキャンセルの場合は独自の処理を実行する
- 独自の処理で、キャンセル専用のアイコンを表示している
1つ目の場合、3rd party用の外部公開APIを流用できますが、公開されているAPIにわざわざ隠し値を用意するのか?という疑問が残ります。
2つ目の場合、GitHub Actionsの都合に合わせた実装が可能です。もしかしたらキャンセル専用のアイコンを使うだけでなく、バックグラウンドでキャンセルに応じた何かしらの処理を行なっている可能性があります。ただ、キャンセルのためだけに独自処理を実装するのだろうか?と思います。
個人的には2つ目が有力と考えています。2つ目がカスタマイズしやすそうはもちろんありますが、1つ目のAPI隠し値についてリスクが大きいため、2つ目が良いと思われるためです。
「公開されたAPIを額面通りにそのまま受け取らないこと」ということは、ドキュメントへの不信感を増す1つとなり得ます。そんなリスクが高い案の採用は考えづらいです。
おわりに
今回の記事では、自作CIサーバー構築によるGitHubのcommit statusアイコンの違いについて調査しました。明確な答えは見つかりませんでしたが、公開されている仕様書を元に考察していくのは楽しいですね。
この記事が誰かのお役に立てれば幸いです。
Discussion