はじめに
このガイドでは、Github App と Checks API について紹介します。Checks API は、テストを実行する継続的インテグレーション (CI) サーバーを構築するために使用します。
CI とは、ソフトウェアの開発においてコードを頻繁に共有リポジトリにコミットする手法のことです。 コードをコミットする頻度が高いほどエラーの発生が早くなり、開発者がエラーの原因を見つけようとしてデバッグする必要性も減ります。 コードの更新が頻繁であれば、ソフトウェア開発チームの他のメンバーによる変更をマージするのも、それだけ容易になります。 開発者がコードの記述にばかり時間をとられ、エラーのデバッグやマージコンフリクトの解決にかける時間が少ないときに威力を発揮します。 🙌
CI サーバーは、コードの文法チェッカー (スタイルフォーマットをチェックする)、セキュリティチェック、コード網羅率、その他のチェックといった CI テストをリポジトリの新しいコードコミットに対して実行するコードをホストします。 CI サーバーは、ステージングサーバーや本番サーバーにコードを構築しデプロイすることも可能です。 GitHub App で作成できる CI テストのタイプの例については、GitHub Marketplace で入手できる継続的インテグレーションアプリケーションを確認してください。
Note: This guide demonstrates the app development process using the Ruby programming language. However, there are many flavors of Octokit. If you prefer JavaScript, you can use Probot and Node.js to develop GitHub Apps.
Checks API の概要
Checks API を使用すると、リポジトリでコミットされている各コードに対して自動的に実行される CI テストを設定できます。 Checks API は、プルリクエストの [Checks] タブにおいて、各チェックについての詳細情報をレポートします。 Checks API を使用すると、コードの特定の行に対して追加的な情報を含むアノテーションを作成できます。 アノテーションは [Checks] タブに表示されます。 プルリクエストの一部であるファイルに対してアノテーションを作成すると、そのアノテーションは [Files changed] タブにも表示されます。
チェックスイートとは、 チェック実行 (個々の CI テスト) をグループ化したものです。 チェックスイートにもチェック実行にもステータスが含まれており、GitHub のプルリクエストで表示できます。 ステータスを使用して、コードコミットがエラーを発生させるタイミングを決定できます。 これらのステータスを保護されたブランチで使用すると、プルリクエストを早まってマージすることを防げます。 詳細は「ステータスチェック必須の有効化」を参照してください。
Checks API は、新しいコードがリポジトリにプッシュされるたびに、リポジトリにインストールされている全ての GitHub App に check_suite
webhook イベントを送信します。 Checks API イベントの全てのアクションを受信するには、アプリケーションに checks:write
権限が必要です。 GitHub はデフォルトのフローを使ってリポジトリの新しいコードのコミットに check_suite
イベントを自動的に作成しますが、チェックスイートのためのリポジトリプリファレンスの更新を行っても構いません。 デフォルトのフローは以下の通りです。
- 誰かがリポジトリにコードをプッシュすると、GitHubは、
checks:write
権限を持つ、リポジトリにインストールされている全ての GitHub Apps にrequested
のアクションと共にcheck_suite
イベントを送信します。 このイベントにより、コードがプッシュされたことと、GitHub が新しいチェックスイートを自動的に作成したことがアプリケーションに通知されます。 - アプリケーションがこのイベントを受信すると、アプリケーションはスイートにチェック実行を追加できます。
- チェック実行には、コードの特定の行で表示されるアノテーションを含めることができます。
In this guide, you’ll learn how to:
- パート 1: Checks API を使用して CI サーバー用のフレームワークをセットアップする。
- Checks API イベントを受信するサーバーとして GitHub App を構成します。
- Create new check runs for CI tests when a repository receives newly pushed commits.
- Re-run check runs when a user requests that action on GitHub.
- Part 2: Build on the CI server framework you created by adding a linter CI test.
- Update a check run with a
status
,conclusion
, andoutput
details. - Create annotations on lines of code that GitHub displays in the Checks and Files Changed tab of a pull request.
- Automatically fix linter recommendations by exposing a "Fix this" button in the Checks tab of the pull request.
- Update a check run with a
このクイックスタートを完了したときに Checks API CI サーバーがどのように動作するかを理解するには、以下のデモをご覧ください。
必要な環境
以下の作業に取りかかる前に、Github Apps、webhook、Checks API を使い慣れていない場合は、ある程度慣れておくとよいでしょう。 REST API ドキュメントには、さらに API が掲載されています。 Checks API は GraphQL でも使用できますが、このクイックスタートでは REST に焦点を当てます。 詳細については、GraphQL Checks Suite および Check Run オブジェクトを参照してください。
You'll use the Ruby programming language, the Smee webhook payload delivery service, the Octokit.rb Ruby library for the GitHub REST API, and the Sinatra web framework to create your Checks API CI server app.
このプロジェクトを完了するために、これらのツールや概念のエキスパートである必要はありません。 このガイドでは、必要なステップを順番に説明していきます。 Checks API で CI テストを作成する前に、以下を行う必要があります。
-
Checks API で CI テストを作成する リポジトリをクローンします。
$ git clone https://github.com/github-developer/creating-ci-tests-with-the-checks-api.git
ディレクトリの中には、このクイックスタートで使用する
template_server.rb
ファイルと、完成したプロジェクトコードであるserver.rb
ファイルがあります。 -
「開発環境をセットアップする」クイックスタートに記載されたステップに従い、アプリケーションサーバーを構成して実行します。 注釈: GitHub App のテンプレートリポジトリをクローンするのではなく、このクイックスタートの直前のステップでクローンしたリポジトリにある
template_server.rb
ファイルを使用します。GitHub App クイックスタートガイドを以前に完了している場合、このクイックスタートでは必ず新たな GitHub App を登録し、このクイックスタートで使用する Smee チャンネルを新しく開始するようにしてください。
テンプレート GitHub App の設定で問題にぶつかった場合は、トラブルシューティングのセクションを参照してください。
パート1. Checks API インターフェースを作成する
このパートでは、check_suite
webhook イベントを受信するために必要なコードを追加し、チェック実行を作成して更新します。 また、GitHub でチェックが再リクエストされた場合にチェック実行を作成する方法についても学びます。 At the end of this section, you'll be able to view the check run you created in a GitHub pull request.
このセクションでは、作成したチェック実行はコードでチェックを実行しません。 この機能については、パート 2: Octo RuboCop CI テストを作成するで追加します。
ローカルサーバーにwebhook ペイロードを転送するよう Smee チャンネルが構成されているでしょうか。 サーバーは実行中で、登録済みかつテストリポジトリにインストールした GitHub App に接続している必要があります。 「開発環境をセットアップする」のステップを完了していない場合は、次に進む前にこれを実行する必要があります。
さあ、始めましょう! パート 1 では、以下のステップを完了させます。
ステップ 1.1. アプリケーションの権限を更新する
最初にアプリケーションを登録した際は、デフォルトの権限を受け入れています。これは、アプリケーションがほとんどのリソースにアクセスできないことを意味します。 この例においては、アプリケーションにはチェックを読み取りおよび書き込みする権限が必要となります。
アプリケーションの権限を更新するには、以下の手順に従います。
- アプリケーションの設定ページからアプリケーションを選択肢、サイドバーの [Permissions & Webhooks] をクリックします。
- [Permissions] セクションで [Checks] を見つけて、隣にある [Access] ドロップダウンで [Read & write] を選択します。
- [Subscribe to events] セクションで [Check suite] と [Check run] を選択してこれらのイベントをサブスクライブします。
- Click Save changes at the bottom of the page.
- If you've installed the app on your account, check your email and follow the link to accept the new permissions. Any time you change your app's permissions or webhooks, users who have installed the app (including yourself) will need to accept the new permissions before the changes take effect. You can also accept the new permissions by navigating to your installations page and clicking on "Configure" next to your app. You'll see a banner at the top of the page letting you know that the app is requesting different permissions. Click "Details" and click "Accept new permissions."
これでうまくいきました。 アプリケーションは必要なタスクを実行する権限を所有しています。 これでイベントを処理するコードを追加できるようになりました。
ステップ 1.2. イベントの処理を追加する
ここまでで、アプリケーションが Check suite および Check run イベントにサブスクライブされ、check_suite
および check_run
webhook を受信し始めます。 GitHub は webhook ペイロードを POST
リクエストとして送信します。 Smee webhook ペイロードを http://localhost/event_handler:3000
に転送したため、サーバーは POST
リクエストのペイロードを post '/event_handler'
ルートで受信します。
空の post '/event_handler'
ルートは、必要な環境セクションでダウンロードした template_server.rb
ファイルに既に含まれています。 空のルートは次のようになっています。
post '/event_handler' do
# # # # # # # # # # # #
# ADD YOUR CODE HERE #
# # # # # # # # # # # #
200 # success status
end
次のコードを追加し、このルートを使用して check_suite
イベントを処理します。
# Get the event type from the HTTP_X_GITHUB_EVENT header
case request.env['HTTP_X_GITHUB_EVENT']
when 'check_suite'
# A new check_suite has been created. Create a new check run with status queued
if @payload['action'] == 'requested' || @payload['action'] == 'rerequested'
create_check_run
end
end
GitHub が送信する全てのイベントには、HTTP_X_GITHUB_EVENT
というリクエストヘッダが含まれており、これは POST
リクエストのイベントの型を示します。 ここでは check_suite
型のイベントにのみ注目しましょう。これは新しいチェックスイートが作成された時に発生します。 各イベントには、アクションをトリガーしたイベントのタイプを示す action
フィールドが付いています。 check_suite
では、action
フィールドは requested
、rerequested
、completed
のいずれかとなります。
requested
アクションはリポジトリにコードがプッシュされるたびにチェック実行をリクエストし、rerequested
アクションはリポジトリに既存のコードにチェックを再実行するようリクエストします。 requested
と rerequested
の両方のアクションでチェック実行の作成が必要なため、create_check_run
というヘルパーを呼び出します。 では、このメソッドを書いてみましょう。
ステップ 1.3. チェック実行を作成する
他のルートでも使用する場合のために、新しいメソッドを Sinatra ヘルパー として追加します。 helpers do
の下に、以下の create_check_run
メソッドを追加します。
# Create a new check run with the status queued
def create_check_run
# # At the time of writing, Octokit does not support the Checks API yet, but
# it does provide generic HTTP methods you can use:
# /v3/checks/runs/#create-a-check-run
check_run = @installation_client.post(
"repos/#{@payload['repository']['full_name']}/check-runs",
{
# This header allows for beta access to Checks API
accept: 'application/vnd.github.antiope-preview+json',
# The name of your check run.
name: 'Octo RuboCop',
# The payload structure differs depending on whether a check run or a check suite event occurred.
head_sha: @payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha']
}
)
end
このコードは HTTP POST
メソッドを使用して、「チェック実行の作成」エンドポイントを呼び出します。 このメソッドは、エンドポイントの URL とメソッドの入力パラメータという 2 つのパラメータを取ります。
チェック実行を作成するために必要なのは、name
と head_sha
の 2 つの入力パラメータのみです。 このクイックスタートでは、後で Rubocop を使用して CI テストを実装します。そのため、ここでは「Octo Rubocop」という名前を使っていますが、チェック実行には任意の名前を選ぶことができます。
ここでは基本的な機能を実行するため必要なパラメータのみを指定していますが、チェック実行について必要な情報を収集するため、後でチェック実行を更新することになります。 デフォルトでは、GitHub は status
を queued
に設定します。
GitHub は特定のコミット SHA に対するチェック実行を作成します。これが head_sha
が必須パラメータである理由です。 コミット SHA は、webhook ペイロードで確認できます。 現時点では check_suite
イベントにチェック実行を作成しているだけですが、head_sha
がイベントペイロードの check_suite
と check_run
の両方のオブジェクトに含まれていることは知っておくとよいでしょう。
上記のコードでは、if/else
文のように機能する三項演算子を使用して、ペイロードが check_run
オブジェクトを含んでいるか確認しています。 含んでいる場合、check_run
オブジェクトから head_sha
を読み取り、含んでいない場合は check_suite
オブジェクトから読み取ります。
このコードをテストするには、サーバーをターミナルから再起動します。
$ ruby template_server.rb
Note: You'll need to restart the Sinatra server before you can test changes. Enter Ctrl-C
to stop the server, and then run ruby template_server.rb
again. If you don't want to do this every time you change your app code, you can look into reloading.
さて、それではアプリケーションをインストールしたリポジトリにあるプルリクエストを開いてください。 アプリケーションは応答し、プルリクエストのチェック実行を作成するはずです。 [Checks] タブをクリックすると、画面が以下のようになっているはずです。
[Checks] タブに他のアプリケーションが表示されている場合は、チェックに対して読み取りおよび書き込みアクセス権を持ち、Check suite および Check run イベントにサブスクライブしている他のアプリケーションをリポジトリにインストールしているものと思われます。
これでうまくいきました。 ここまでで、GitHub にチェック実行を作成するよう指示しました。 チェック実行のステータスが queued
に設定されていることが、黄色のアイコンの右側で確認できます。 次は、GitHub がチェック実行を作成し、ステータスを更新するのを待てばよいでしょう。
ステップ 1.4. チェック実行を更新する
create_check_run
メソッドが実行されると、メソッドは GitHub に新しいチェック実行を作成するよう依頼します。 Github がチェック実行の作成を完了すると、created
アクションの check_run
webhook イベントを受信します。 このイベントは、チェックの実行が始まる合図です。
イベントハンドラーを更新し、created
アクションを待ち受けるようにしましょう。 イベントハンドラーを更新する際、rerequested
アクションに条件を追加できます。 誰かが [Re-run] ボタンをクリックして GitHub 上で単一のテストを再実行すると、GitHub はアプリケーションに rerequested
チェック実行イベントを送信します。 チェック実行が rerequested
の場合、すべてのプロセスを開始し、新しいチェック実行を作成します。
To include a condition for the event in the post '/event_handler'
ルートに check_run
イベントの条件を含めるには、case request.env['HTTP_X_GITHUB_EVENT']
の下に次のコードを追加します。
when 'check_run'
# Check that the event is being sent to this app
if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER
case @payload['action']
when 'created'
initiate_check_run
when 'rerequested'
create_check_run
end
end
GitHub は created
チェック実行のすべてのイベントを、必要なチェック権限を持つリポジトリにインストールされたあらゆるアプリケーションに送信します。 これはつまり、あなたのアプリケーションが他のアプリケーションにより作成されたチェック実行を受信するということです。 created
チェック実行は、チェックを要求されているアプリケーションのみに GitHub が送信する requested
や rerequested
チェックスイートとは少し違います。 上記のコードは、チェック実行のアプリケーション ID を待ち受けます。 リポジトリの他のアプリケーションに対するチェック実行はすべて遮断されます。
次に initiate_check_run
メソッドを書きます。これは、チェック実行のステータスを更新し、CI テストの開始を準備するものです。
このセクションでは、まだ CI テストは開始しません。その代わり、チェック実行のステータスを queued
から pending
に、そしてその後 pending
から completed
に更新する手順を確認し、チェック実行のフロー全体を確認します。 「パート2: Octo RuboCop CI テストを作成する」では、CI テストを実際に実行するコードを追加します。
initiate_check_run
メソッドを作成し、チェック実行のステータスを更新しましょう。 以下のコードを helpers セクションに追加します。
# Start the CI process
def initiate_check_run
# Once the check run is created, you'll update the status of the check run
# to 'in_progress' and run the CI process. When the CI finishes, you'll
# update the check run status to 'completed' and add the CI results.
# Octokit doesn't yet support the Checks API, but it does provide generic
# HTTP methods you can use:
# /v3/checks/runs/#update-a-check-run
updated_check_run = @installation_client.patch(
"repos/#{@payload['repository']['full_name']}/check-runs/#{@payload['check_run']['id']}",
{
accept: 'application/vnd.github.antiope-preview+json', # This header is necessary for beta access to Checks API
name: 'Octo RuboCop',
status: 'in_progress',
started_at: Time.now.utc.iso8601
}
)
# ***** RUN A CI TEST *****
# Mark the check run as complete!
updated_check_run = @installation_client.patch(
"repos/#{@payload['repository']['full_name']}/check-runs/#{@payload['check_run']['id']}",
{
# This header is necessary for beta access to Checks API
accept: 'application/vnd.github.antiope-preview+json',
name: 'Octo RuboCop',
status: 'completed',
conclusion: 'success',
completed_at: Time.now.utc.iso8601
}
)
end
上記のコードは、ジェネリックな patch
HTTP methodメソッドを使用して「チェック実行を更新する」API エンドポイントを呼び出し、既に作成したチェック実行を更新します。
このコードがしていることを説明しましょう。 まず、チェック実行のステータスを in_progress
に更新し、started_at
の時刻を現在の時刻に設定します。 このクイックスタートのパート 2では、実際の CI テストを開始するコードを ***** RUN A CI TEST *****
の下に追加します。 今はこのセクションをプレースホルダーとして残しておきましょう。そうすると、続くコードが CI のプロセスを成功させ、すべてのテストに合格したことをシミュレートすることになります。 最後に、コードはチェック実行のステータスを再び completed
に更新します。
「チェック実行を更新する」 ドキュメントに、completed
のステータスを指定すると、conclusion
と completed_at
のパラメータが必須となることが書かれています。 conclusion
はチェック実行の結果を要約するもので、success
、failure
、neutral
、cancelled
、timed_out
、action_required
のいずれかになります。 この結果 (conclusion) は success
に、completed_at
の時刻は現在の時刻に、ステータスは completed
に設定します。
チェックが行っていることについてより詳しく指定することもできますが、それは次のセクションで行うことにします。 では、template_server.rb
を実行して、このコードを再びテストしましょう。
$ ruby template_server.rb
開いたプルリクエストに移動し、[Checks] タブをクリックします。 左上隅にある [Re-run all] ボタンをクリックしてください。 チェック実行が pending
から in_progress
に移動し、success
で終わることが確認できるはずです。
パート2. Octo RuboCop CI テストを作成する
RuboCop は Ruby のコード文法チェッカーおよびフォーマッタです。 Ruby のコードが「Ruby スタイルガイド」に準拠するようチェックします。 RuboCop の主な機能は、以下の 3 つです。
- コードのスタイルを確認する文法チェック
- コードの整形
- Replaces the native Ruby linting capabilities using
ruby -w
さて、Checks API を受信し、チェック実行を作成するために作ったインターフェースができあがったところで、今度は CI テストを実装するチェック実行を作成しましょう。
あなたのアプリケーションは CI サーバー上の RuboCop で実行され、結果を RuboCop が GitHub に報告するチェック実行 (ここでは CI テスト) を作成します。
Checks API を使用すると、ステータス、画像、要約、アノテーション、リクエストされたアクションなどの、各チェック実行の詳細情報を報告できます。
アノテーションとは、リポジトリのコードの特定の行についての情報です。 アノテーションを使用すると、追加情報を表示したいコードの部分を細かく指定して、それを視覚化できます。 この情報は、たとえばコメント、エラー、警告など何でも構いません。 このクイックスタートでは、RuboCop のエラーを視覚化するためにアノテーションを使用します。
リクエストされたアクションを利用るため、アプリケーション開発者はプルリクエストの [Checks] タブにボタンを作成できます。 このボタンがクリックされると、そのクリックにより GitHub App に requested_action
check_run
イベントが送信されます。 アプリケーションが行うアクションは、アプリケーション開発者が自由に設定できます。 このクイックスタートでは、RuboCop が見つけたエラーを修正するようユーザがリクエストするためのボタンを追加する方法について説明します。 RuboCop はコマンドラインオプションによるエラーの自動的な修正をサポートしており、ここでは requested_action
を設定して、このオプションを使用できるようにします。
さあ、始めましょう! このセクションでは、以下のステップを完了させます。
- Ruby ファイルを追加する
- リポジトリをクローンする
- RuboCop を実行する
- RuboCop のエラーを収集する
- CI テスト結果でチェック実行を更新する
- RuboCop のエラーを自動的に修正する
- セキュリティのヒント
ステップ 2.1. Ruby ファイルを追加する
RuboCop がチェックするため、特定のファイルまたはディレクトリ全体を渡すことができます。 このクイックスタートでは、ディレクトリ全体で RuboCop を実行します。 RuboCop がチェックするのは Ruby のコードのみなので、エラーが含まれる Ruby ファイルをリポジトリ内に最低 1 つ置くとよいでしょう。 以下に示すサンプルのファイルには、いくつかのエラーが含まれています。 Add this example Ruby file to the repository where your app is installed (make sure to name the file with an .rb
extension, as in myfile.rb
):
# The Octocat class tells you about different breeds of Octocat
class Octocat
def initialize(name, *breeds)
# Instance variables
@name = name
@breeds = breeds
end
def display
breed = @breeds.join("-")
puts "I am of #{breed} breed, and my name is #{@name}."
end
end
m = Octocat.new("Mona", "cat", "octopus")
m.display
ステップ 2.2. リポジトリをクローンする
RuboCop is available as a command-line utility. That means your GitHub App will need to clone a local copy of the repository on the CI server so RuboCop can parse the files. To run Git operations in your Ruby app, you can use the ruby-git gem.
The Gemfile
in the building-a-checks-api-ci-server
repository already includes the ruby-git gem, and you installed it when you ran bundle install
in the prerequisite steps. To use the gem, add this code to the top of your template_server.rb
file:
require 'git'
Your app needs read permission for "Repository contents" to clone a repository. Later in this quickstart, you'll need to push contents to GitHub, which requires write permission. Go ahead and set your app's "Repository contents" permission to Read & write now so you don't need to update it again later. アプリケーションの権限を更新するには、以下の手順に従います。
- アプリケーションの設定ページからアプリケーションを選択肢、サイドバーの [Permissions & Webhooks] をクリックします。
- In the "Permissions" section, find "Repository contents", and select Read & write in the "Access" dropdown next to it.
- Click Save changes at the bottom of the page.
- If you've installed the app on your account, check your email and follow the link to accept the new permissions. Any time you change your app's permissions or webhooks, users who have installed the app (including yourself) will need to accept the new permissions before the changes take effect. You can also accept the new permissions by navigating to your installations page and clicking on "Configure" next to your app. You'll see a banner at the top of the page letting you know that the app is requesting different permissions. Click "Details" and click "Accept new permissions."
To clone a repository using your GitHub App's permissions, you can use the app's installation token (x-access-token:<token>
) shown in the example below:
git clone https://x-access-token:@github.com//.git
The code above clones a repository over HTTP. It requires the full repository name, which includes the repository owner (user or organization) and the repository name. For example, the octocat Hello-World repository has a full name of octocat/hello-world
.
After your app clones the repository, it needs to pull the latest code changes and check out a specific Git ref. The code to do all of this will fit nicely into its own method. To perform these operations, the method needs the name and full name of the repository and the ref to checkout. The ref can be a commit SHA, branch, or tag. Add the following new method to the helper method section in template_server.rb
:
# Clones the repository to the current working directory, updates the
# contents using Git pull, and checks out the ref.
#
# full_repo_name - The owner and repo. Ex: octocat/hello-world
# repository - The repository name
# ref - The branch, commit SHA, or tag to check out
def clone_repository(full_repo_name, repository, ref)
@git = Git.clone("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", repository)
pwd = Dir.getwd()
Dir.chdir(repository)
@git.pull
@git.checkout(ref)
Dir.chdir(pwd)
end
The code above uses the ruby-git
gem to clone the repository using the app's installation token. This code clones the code in the same directory as template_server.rb
. To run Git commands in the repository, the code needs to change into the repository directory. Before changing directories, the code stores the current working directory in a variable (pwd
) to remember where to return before exiting the clone_repository
method.
From the repository directory, this code fetches and merges the latest changes (@git.pull
), checks out the ref (@git.checkout(ref)
), then changes the directory back to the original working directory (pwd
).
Now you've got a method that clones a repository and checks out a ref. Next, you need to add code to get the required input parameters and call the new clone_repository
method. Add the following code under the ***** RUN A CI TEST *****
comment in your initiate_check_run
helper method:
# ***** RUN A CI TEST *****
full_repo_name = @payload['repository']['full_name']
repository = @payload['repository']['name']
head_sha = @payload['check_run']['head_sha']
clone_repository(full_repo_name, repository, head_sha)
The code above gets the full repository name and the head SHA of the commit from the check_run
webhook payload.
ステップ 2.3. RuboCop を実行する
これでうまくいきました。 You're cloning the repository and creating check runs using your CI server. Now you'll get into the nitty gritty details of the RuboCop linter and Checks API annotations.
The following code runs RuboCop and saves the style code errors in JSON format. Add this code below the call to clone_repository
you added in the previous step and above the code that updates the check run to complete.
# Run RuboCop on all files in the repository
@report = `rubocop '#{repository}' --format json`
logger.debug @report
`rm -rf #{repository}`
@output = JSON.parse @report
The code above runs RuboCop on all files in the repository's directory. The option --format json
is a handy way to save a copy of the linting results in a machine-parsable format. See the RuboCop docs for details and an example of the JSON format.
Because this code stores the RuboCop results in a @report
variable, it can safely remove the checkout of the repository. This code also parses the JSON so you can easily access the keys and values in your GitHub App using the @output
variable.
Note: The command used to remove the repository (rm -rf
) cannot be undone. See Step 2.7. Security tips to learn how to check webhooks for injected malicious commands that could be used to remove a different directory than intended by your app. For example, if a bad actor sent a webhook with the repository name ./
, your app would remove the root directory. 😱 If for some reason you're not using the method verify_webhook_signature
(which is included in template_server.rb
) to validate the sender of the webhook, make sure you check that the repository name is valid.
You can test that this code works and see the errors reported by RuboCop in your server's debug output. Start up the template_server.rb
server again and create a new pull request in the repository where you're testing your app:
$ ruby template_server.rb
You should see the linting errors in the debug output, although they aren't printed with formatting. You can use a web tool like JSON formatter to format your JSON output like this formatted linting error output:
{
"metadata": {
"rubocop_version": "0.60.0",
"ruby_engine": "ruby",
"ruby_version": "2.3.7",
"ruby_patchlevel": "456",
"ruby_platform": "universal.x86_64-darwin18"
},
"files": [
{
"path": "Octocat-breeds/octocat.rb",
"offenses": [
{
"severity": "convention",
"message": "Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.",
"cop_name": "Style/StringLiterals",
"corrected": false,
"location": {
"start_line": 17,
"start_column": 17,
"last_line": 17,
"last_column": 22,
"length": 6,
"line": 17,
"column": 17
}
},
{
"severity": "convention",
"message": "Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.",
"cop_name": "Style/StringLiterals",
"corrected": false,
"location": {
"start_line": 17,
"start_column": 25,
"last_line": 17,
"last_column": 29,
"length": 5,
"line": 17,
"column": 25
}
}
]
}
],
"summary": {
"offense_count": 2,
"target_file_count": 1,
"inspected_file_count": 1
}
}
ステップ 2.4. RuboCop のエラーを収集する
The @output
variable contains the parsed JSON results of the RuboCop report. As shown above, the results contain a summary
section that your code can use to quickly determine if there are any errors. The following code will set the check run conclusion to success
when there are no reported errors. RuboCop reports errors for each file in the files
array, so if there are errors, you'll need to extract some data from the file object.
The Checks API allows you to create annotations for specific lines of code. When you create or update a check run, you can add annotations. In this quickstart you are updating the check run with annotations.
The Checks API limits the number of annotations to a maximum of 50 per API request. To create more than 50 annotations, you have to make multiple requests to the Update a check run endpoint. For example, to create 105 annotations you'd need to call the Update a check run endpoint three times. The first two requests would each have 50 annotations, and the third request would include the five remaining annotations. Each time you update the check run, annotations are appended to the list of annotations that already exist for the check run.
A check run expects annotations as an array of objects. Each annotation object must include the path
, start_line
, end_line
, annotation_level
, and message
. RuboCop provides the start_column
and end_column
too, so you can include those optional parameters in the annotation. Annotations only support start_column
and end_column
on the same line. See the annotations
object reference documentation for details.
You'll extract the required information from RuboCop needed to create each annotation. Append the following code to the code you added in the previous section:
annotations = []
# You can create a maximum of 50 annotations per request to the Checks
# API. To add more than 50 annotations, use the "Update a check run" API
# endpoint. This example code limits the number of annotations to 50.
# See /v3/checks/runs/#update-a-check-run
# for details.
max_annotations = 50
# RuboCop reports the number of errors found in "offense_count"
if @output['summary']['offense_count'] == 0
conclusion = 'success'
else
conclusion = 'neutral'
@output['files'].each do |file|
# Only parse offenses for files in this app's repository
file_path = file['path'].gsub(/#{repository}\//,'')
annotation_level = 'notice'
# Parse each offense to get details and location
file['offenses'].each do |offense|
# Limit the number of annotations to 50
next if max_annotations == 0
max_annotations -= 1
start_line = offense['location']['start_line']
end_line = offense['location']['last_line']
start_column = offense['location']['start_column']
end_column = offense['location']['last_column']
message = offense['message']
# Create a new annotation for each error
annotation = {
path: file_path,
start_line: start_line,
end_line: end_line,
start_column: start_column,
end_column: end_column,
annotation_level: annotation_level,
message: message
}
# Annotations only support start and end columns on the same line
if start_line == end_line
annotation.merge({start_column: start_column, end_column: end_column})
end
annotations.push(annotation)
end
end
end
This code limits the total number of annotations to 50. But you can modify this code to update the check run for each batch of 50 annotations. The code above includes the variable max_annotations
that sets the limit to 50, which is used in the loop that iterates through the offenses.
When the offense_count
is zero, the CI test is a success
. If there are errors, this code sets the conclusion to neutral
in order to prevent strictly enforcing errors from code linters. But you can change the conclusion to failure
if you would like to ensure that the check suite fails when there are linting errors.
When errors are reported, the code above iterates through the files
array in the RuboCop report. For each file, it extracts the file path and sets the annotation level to notice
. You could go even further and set specific warning levels for each type of RuboCop Cop, but to keep things simpler in this quickstart, all errors are set to a level of notice
.
This code also iterates through each error in the offenses
array and collects the location of the offense and error message. After extracting the information needed, the code creates an annotation for each error and stores it in the annotations
array. Because annotations only support start and end columns on the same line, start_column
and end_column
are only added to the annotation
object if the start and end line values are the same.
This code doesn't yet create an annotation for the check run. You'll add that code in the next section.
ステップ 2.5. CI テスト結果でチェック実行を更新する
Each check run from GitHub contains an output
object that includes a title
, summary
, text
, annotations
, and images
. The summary
and title
are the only required parameters for the output
, but those alone don't offer much detail, so this quickstart adds text
and annotations
too. The code here doesn't add an image, but feel free to add one if you'd like!
For the summary
, this example uses the summary information from RuboCop and adds some newlines (\n
) to format the output. You can customize what you add to the text
parameter, but this example sets the text
parameter to the RuboCop version. To set the summary
and text
, append this code to the code you added in the previous section:
# Updated check run summary and text parameters
summary = "Octo RuboCop summary\n-Offense count: #{@output['summary']['offense_count']}\n-File count: #{@output['summary']['target_file_count']}\n-Target file count: #{@output['summary']['inspected_file_count']}"
text = "Octo RuboCop version: #{@output['metadata']['rubocop_version']}"
Now you've got all the information you need to update your check run. In the first half of this quickstart, you added this code to set the status of the check run to success
:
# Mark the check run as complete!
updated_check_run = @installation_client.patch(
"repos/#{@payload['repository']['full_name']}/check-runs/#{@payload['check_run']['id']}",
{
accept: 'application/vnd.github.antiope-preview+json', # This header is necessary for beta access to Checks API
name: 'Octo RuboCop',
status: 'completed',
conclusion: 'success',
completed_at: Time.now.utc.iso8601
}
)
You'll need to update that code to use the conclusion
variable you set based on the RuboCop results (to success
or neutral
). You can update the code with the following:
# Mark the check run as complete! And if there are warnings, share them.
updated_check_run = @installation_client.patch(
"repos/#{@payload['repository']['full_name']}/check-runs/#{@payload['check_run']['id']}",
{
accept: 'application/vnd.github.antiope-preview+json',
name: 'Octo RuboCop',
status: 'completed',
conclusion: conclusion,
completed_at: Time.now.utc.iso8601,
output: {
title: 'Octo RuboCop',
summary: summary,
text: text,
annotations: annotations
},
actions: [{
label: 'Fix this',
description: 'Automatically fix all linter notices.',
identifier: 'fix_rubocop_notices'
}]
}
)
Now that you're setting a conclusion based on the status of the CI test and you've added the output from the RuboCop results, you've created a CI test! Congratulations. 🙌
The code above also adds a feature to your CI server called requested actions via the actions
object. Requested actions add a button in the Checks tab on GitHub that allows someone to request the check run to take additional action. The additional action is completely configurable by your app. For example, because RuboCop has a feature to automatically fix the errors it finds in Ruby code, your CI server can use a requested actions button to allow people to request automatic error fixes. When someone clicks the button, the app receives the check_run
event with a requested_action
action. Each requested action has an identifier
that the app uses to determine which button was clicked.
The code above doesn't have RuboCop automatically fix errors yet. You'll add that in the next section. But first, take a look at the CI test that you just created by starting up the template_server.rb
server again and creating a new pull request:
$ ruby template_server.rb
The annotations will show up in the Checks tab.
Notice the "Fix this" button that you created by adding a requested action.
If the annotations are related to a file already included in the PR, the annotations will also show up in the Files changed tab.
ステップ 2.6. RuboCop のエラーを自動的に修正する
If you've made it this far, kudos! 👏 You've already created a CI test. In this section, you'll add one more feature that uses RuboCop to automatically fix the errors it finds. You already added the "Fix this" button in the previous section. Now you'll add the code to handle the requested_action
check run event triggered when someone clicks the "Fix this" button.
The RuboCop tool offers the --auto-correct
command-line option to automatically fix errors it finds. When you use the --auto-correct
feature, the updates are applied to the local files on the server. You'll need to push the changes to GitHub after RuboCop does its magic.
To push to a repository, your app must have write permissions for "Repository contents." You set that permission back in Step 2.2. Cloning the repository to Read & write, so you're all set.
In order to commit files, Git must know which username and email to associate with the commit. Add two more environment variables in your .env
file to store the name (GITHUB_APP_USER_NAME
) and email (GITHUB_APP_USER_EMAIL
) settings. Your name can be the name of your app and the email can be any email you'd like for this example. 例:
GITHUB_APP_USER_NAME=Octoapp
GITHUB_APP_USER_EMAIL=octoapp@octo-org.com
Once you've updated your .env
file with the name and email of the author and committer, you'll be ready to add code to read the environment variables and set the Git configuration. You'll add that code soon.
When someone clicks the "Fix this" button, your app receives the check run webhook with the requested_action
action type.
In Step 1.4. Updating a check run you updated the your event_handler
to handle look for actions in the check_run
event. You already have a case statement to handle the created
and rerequested
action types:
when 'check_run'
# Check that the event is being sent to this app
if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER
case @payload['action']
when 'created'
initiate_check_run
when 'rerequested'
create_check_run
end
end
Add another when
statement after the rerequested
case to handle the rerequested_action
event:
when 'requested_action'
take_requested_action
This code calls a new method that will handle all requested_action
events for your app. Add the following method to the helper methods section of your code:
# Handles the check run `requested_action` event
# See /webhooks/event-payloads/#check_run
def take_requested_action
full_repo_name = @payload['repository']['full_name']
repository = @payload['repository']['name']
head_branch = @payload['check_run']['check_suite']['head_branch']
if (@payload['requested_action']['identifier'] == 'fix_rubocop_notices')
clone_repository(full_repo_name, repository, head_branch)
# Sets your commit username and email address
@git.config('user.name', ENV['GITHUB_APP_USER_NAME'])
@git.config('user.email', ENV['GITHUB_APP_USER_EMAIL'])
# Automatically correct RuboCop style errors
@report = `rubocop '#{repository}/*' --format json --auto-correct`
pwd = Dir.getwd()
Dir.chdir(repository)
begin
@git.commit_all('Automatically fix Octo RuboCop notices.')
@git.push("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", head_branch)
rescue
# Nothing to commit!
puts 'Nothing to commit'
end
Dir.chdir(pwd)
`rm -rf '#{repository}'`
end
end
The code above clones a repository just like the code you added in Step 2.2. Cloning the repository. An if
statement checks that the requested action's identifier matches the RuboCop button identifier (fix_rubocop_notices
). When they match, the code clones the repository, sets the Git username and email, and runs RuboCop with the option --auto-correct
. The --auto-correct
option applies the changes to the local CI server files automatically.
The files are changed locally, but you'll still need to push them to GitHub. You'll use the handy ruby-git
gem again to commit all of the files. Git has a single command that stages all modified or deleted files and commits them: git commit -a
. To do the same thing using ruby-git
, the code above uses the commit_all
method. Then the code pushes the committed files to GitHub using the installation token, using the same authentication method as the Git clone
command. Finally, it removes the repository directory to ensure the working directory is prepared for the next event.
これで完了です。 The code you have written now completes your Checks API CI server. 💪 Restart your template_server.rb
server again and create a new pull request:
$ ruby template_server.rb
Note: You'll need to restart the Sinatra server before you can test changes. Enter Ctrl-C
to stop the server, and then run ruby template_server.rb
again. If you don't want to do this every time you change your app code, you can look into reloading.
This time, click the "Fix this" button to automatically fix the errors RuboCop found from the Checks tab.
In the Commits tab, you'll see a brand new commit by the username you set in your Git configuration. You may need to refresh your browser to see the update.
Because a new commit was pushed to the repo, you'll see a new check suite for Octo RuboCop in the Checks tab. But this time there are no errors because RuboCop fixed them all. 🎉
You can find the completed code for the app you just built in the server.rb
file in the Creating CI tests with the Checks API repository.
ステップ 2.7. セキュリティのヒント
The template GitHub App code already has a method to verify incoming webhook payloads to ensure they are from a trusted source. If you are not validating webhook payloads, you'll need to ensure that when repository names are included in the webhook payload, the webhook does not contain arbitrary commands that could be used maliciously. The code below validates that the repository name only contains Latin alphabetic characters, hyphens, and underscores. To provide you with a complete example, the complete server.rb
code available in the companion repository for this quickstart includes both the method of validating incoming webhook payloads and this check to verify the repository name.
# This quickstart example uses the repository name in the webhook with
# command-line utilities. For security reasons, you should validate the
# repository name to ensure that a bad actor isn't attempting to execute
# arbitrary commands or inject false repository names. If a repository name
# is provided in the webhook, validate that it consists only of latin
# alphabetic characters, `-`, and `_`.
unless @payload['repository'].nil?
halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil?
end
トラブルシューティング
Here are a few common problems and some suggested solutions. If you run into any other trouble, you can ask for help or advice in the GitHub API Development and Support Forum.
-
Q: My app isn't pushing code to GitHub. I don't see the fixes that RuboCop automatically makes!
A: Make sure you have Read & write permissions for "Repository contents," and that you are cloning the repository with your installation token. See Step 2.2. Cloning the repository for details.
-
Q: I see an error in the
template_server.rb
debug output related to cloning my repository.A: If you see the following error, you haven't deleted the checkout of the repository in one or both of the
initiate_check_run
ortake_requested_action
methods:2018-11-26 16:55:13 - Git::GitExecuteError - git clone '--' 'https://x-access-token:v1.9b2080277016f797074c4debd350745f4257f8dd@github.com/codertocat/octocat-breeds.git' 'Octocat-breeds' 2>&1:fatal: destination path 'Octocat-breeds' already exists and is not an empty directory.:
Compare your code to the
server.rb
file to ensure you have the same code in yourinitiate_check_run
andtake_requested_action
methods. -
Q: New check runs are not showing up in the "Checks" tab on GitHub.
A: Restart Smee and re-run your
template_server.rb
server. -
Q: I do not see the "Re-run all" button in the "Checks" tab on GitHub.
A: Restart Smee and re-run your
template_server.rb
server.
おわりに
After walking through this guide, you've learned the basics of using the Checks API to create a CI server! To review, you:
- Configured your server to receive Checks API events and create check runs.
- Used RuboCop to check code in repositories and create annotations for the errors.
- Implemented a requested action that automatically fixes linter errors.
次のステップ
Here are some ideas for what you can do next:
- Currently, the "Fix this" button is always displayed. Update the code you wrote to display the "Fix this" button only when RuboCop finds errors.
- If you'd prefer that RuboCop doesn't commit files directly to the head branch, you can update the code to create a pull request with a new branch based on the head branch.