🍣

自作CIサーバー構築によるcommit statusアイコンの違いを探る

2023/09/30に公開

こんにちは、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についてのページがありますが、このような違いについては記載されていませんでした。

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks

これは、自作CIサーバーを作って何が起きているのか把握するしかないと思い、CIサーバーを作り始めました。

自作CIサーバー構築手順

CIサーバー構築手順は、GitHubが公開しています。
https://docs.github.com/en/rest/guides/building-a-ci-server?apiVersion=2022-11-28

この例ではSinatraを用いて、構築しています。大まかな流れは以下のとおりです。

  • Sinatraで自作CIサーバーを起動する
  • ngrokを使ってローカルサーバーを外部に公開する
  • GitHub上でPull Requestなどのイベントの通知先となるWebhookを設定
  • Pull Requestイベント発生時commit statusを変更する処理を自作CIサーバーに実装

では、順に見ていきましょう。

Sinatraで自作CIサーバーを起動する

Gemfileにpuma、sinatraを追加し、server.rbを作成します。

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で設定方法のドキュメントが公開されています。
https://docs.github.com/en/webhooks/using-webhooks/creating-webhooks

これを元に、添付画像のように設定します。
注意ポイントとして、Payload URLには、ngrokで公開したURLに加えてsinatraで設定したパス(event_handler)を追加する必要があります。

GUIで設定する方法は以上です。

APIで設定する方法

私が取った設定方法はこちらです。せっかくCIサーバーを作るので、webhookもコードで設定してみようと思い、こちらを選択しました。
こちらもドキュメントが公開されています。
https://docs.github.com/en/free-pro-team@latest/rest/webhooks/repos?apiVersion=2022-11-28#create-a-repository-webhook

APIを叩くためには、GitHubのPersonal Access Token(PAT)が必要です。PAT
は、GitHubの設定画面から作成できます。PAT作成ではrepoadmin:repo_hookの2つのスコープを設定します。

PATを作成したら、APIを叩くコードを作ります。curlでもAPIを叩けますが、Rubyを使ってAPIを叩きます。

webhook.rb
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を使用して行ないます。

https://github.com/octokit/octokit.rb

Pull Requestイベントを受け取ったときに、commit statusを変更する処理を作成します。この処理では以下3つを順に行なっています。

  1. イベントを受け取ったときにcommit statusをpendingに変更
  2. 5秒待機
  3. commit statusをsuccessに変更
server.rb
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メソッドの実装は以下のとおりです。

soctokit.rb/lib/octokit/client/statuses.rb
      # 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

https://github.com/octokit/octokit.rb/blob/e264213d0f6c6729b208761bd1766e7377616339/lib/octokit/client/statuses.rb#L41

@seeにAPIの仕様書が記載されているので、確認します。
stateがcommit statusを表しているので、ここを確認します。

state string Required
The state of the status.
Can be one of: error, failure, pending, success

https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#create-a-commit-status

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