はじめに
このチュートリアルでは、リポジトリにプッシュされた新しいコードのテストを実行する継続的インテグレーション (CI) サーバーを構築する方法について説明します。 このチュートリアルでは、GitHub の REST API を使用して、Webhook イベント check_run
および check_suite
を受信して応答するサーバーとして機能する GitHub App を構築および構成する方法を示します。
このチュートリアルでは、アプリの開発中にご自身のコンピューターまたは codespace をサーバーとして使います。 アプリを運用環境で使用する準備ができたら、アプリを専用サーバーにデプロイする必要があります。
このチュートリアルでは Ruby を使用しますが、サーバーで実行できる任意のプログラミング言語を使用できます。
このチュートリアルは、次の 2 つのパートに分かれています。
- パート 1 では、GitHub の REST API を使用して CI サーバーのフレームワークを設定する方法、リポジトリで新しくプッシュされたコミットを受信したきに CI テストの新しいチェック実行を作成する方法、ユーザーが GitHub に対してそのアクションを要求したときにチェック実行を再実行する方法について説明します。
- パート 2 では、CI サーバーにリンター テストを追加することで、CI テストに機能を追加します。 さらに、pull request の [チェック] および [変更されたファイル] タブに表示される注釈を作成します。また、pull request の [チェック] タブに [これを修正する] ボタンを表示して、リンターによる推奨事項を自動的に修正します。
継続的インテグレーション (CI) について
CI とは、ソフトウェアの開発においてコードを頻繁に共有リポジトリにコミットする手法のことです。 コードをコミットする頻度が高いほどエラーの発生が早くなり、開発者がエラーの原因を見つけようとしてデバッグする必要性も減ります。 コードの更新が頻繁であれば、ソフトウェア開発チームの他のメンバーによる変更をマージするのも、それだけ容易になります。 コードの記述により多くの時間をかけられるようになり、エラーのデバッグやマージコンフリクトの解決にかける時間が減るので、これは開発者にとって素晴らしいやり方です。
CI サーバーは、コードの文法チェッカー (スタイルフォーマットをチェックする)、セキュリティチェック、コード網羅率、その他のチェックといった CI テストをリポジトリの新しいコードコミットに対して実行するコードをホストします。 CI サーバーは、ステージングサーバーや本番サーバーにコードを構築しデプロイすることも可能です。 GitHub App を使用して作成できる CI テストの種類の例については、GitHub Marketplace で入手できる 継続的インテグレーション アプリを参照してください。
チェックについて
GitHub の REST API を使用すると、リポジトリでコードがコミットされるたびに、そのコードに対して自動的に実行される CI テスト (チェック) を設定できます。 この API により、GitHub の pull request の [チェック] タブに、各チェックに関する詳細な情報が報告されます。 リポジトリ内のチェックを使用して、コード コミットでエラーが発生するタイミングを特定できます。
チェックには、チェック実行、チェック スイート、コミット ステータスが含まれます。
- "チェック実行" は、コミットに対して実行される個々の CI テストです。__
- "チェック スイート" は、チェック実行のグループです。__
- "コミット ステータス" は、コミットの状態 (
error
、failure
、pending
、success
など) をマークします。これは、GitHub の pull request に表示されます。__ チェック スイートとチェック実行の両方に、コミット ステータスが含まれます。
GitHub では、リポジトリに新しいコードがコミットされると、既定のフローを使用して check_suite
イベントが自動的に作成されますが、この既定の設定は変更できます。 詳しくは、「チェック スイート用 REST API エンドポイント」を参照してください。 デフォルトのフローは以下の通りです。
- リポジトリにコードがプッシュされると、GitHub により、リポジトリにインストールされており、
checks:write
のアクセス許可を持つすべての GitHub Apps にcheck_suite
イベント (requested
アクションあり) が自動的に送信されます。 このイベントにより、アプリは、コードがリポジトリにプッシュされたこと、および GitHub によって新しいチェック スイートが自動的に作成されたことを認識します。 - アプリでは、このイベントを受信したら、そのスイートにチェック実行を追加できます。
- チェック実行には、特定のコード行に表示されるアノテーションを含めることができます。 注釈は、[チェック] タブに表示されます。pull request の一部であるファイルに対して注釈を作成すると、その注釈も [変更されたファイル] タブに表示されます。詳細については、「チェック実行用 REST API エンドポイント」の
annotations
オブジェクトを参照してください。
チェックの詳しい情報については、「チェック用 REST API エンドポイント」および「REST API を使用してチェックを操作する」を参照してください。
前提条件
このチュートリアルでは、Ruby プログラミング言語の基本を理解していることを前提とします。
始める前に、次の概念を理解しておくことをお勧めします。
チェックは GraphQL API で使用することもできますが、このチュートリアルでは REST API に焦点を当てています。 GraphQL オブジェクトについて詳しくは、GraphQL のドキュメントの「チェック スイート」と「チェック実行」を参照してください。
セットアップ
以下のセクションでは、次のコンポーネントを設定します。
- アプリのコードを格納するリポジトリ。
- Webhook をローカルで受信する方法。
- "チェック スイート" および "チェック実行" Webhook イベントにサブスクライブされ、チェックに対する書き込みアクセス許可を持ち、ローカルで受信できる Webhook URL を使用する GitHub App。
GitHub App のコードを格納するリポジトリを作成する
-
アプリのコードを格納するリポジトリを作成します。 詳しくは、「新しいリポジトリの作成」を参照してください。
-
前の手順のリポジトリをクローンします。 詳しくは、「リポジトリをクローンする」を参照してください。 ローカル クローンまたは GitHub Codespaces を使用できます。
-
ターミナルで、クローンが格納されているディレクトリに移動します。
-
server.rb
という名前の Ruby ファイルを作成します。 このファイルには、アプリのすべてのコードが含まれます。 後でこのファイルにコンテンツを追加します。 -
ディレクトリに
.gitignore
ファイルがまだ含まれていない場合は、.gitignore
ファイルを追加します。 後でこのファイルにコンテンツを追加します。.gitignore
ファイルについて詳しくは、「ファイルを無視する」を参照してください。 -
Gemfile
という名前でファイルを作成します。 このファイルには、Ruby コードに必要な gem 依存関係が記述されます。 次の内容をGemfile
に追加します。Ruby source 'http://rubygems.org' gem 'sinatra', '~> 2.0' gem 'jwt', '~> 2.1' gem 'octokit', '~> 4.0' gem 'puma' gem 'rubocop' gem 'dotenv' gem 'git'
source 'http://rubygems.org' gem 'sinatra', '~> 2.0' gem 'jwt', '~> 2.1' gem 'octokit', '~> 4.0' gem 'puma' gem 'rubocop' gem 'dotenv' gem 'git'
-
config.ru
という名前でファイルを作成します。 このファイルでは、実行する Sinatra サーバーを構成します。 次の内容をconfig.ru
ファイルに追加します。Ruby require './server' run GHAapp
require './server' run GHAapp
Webhook プロキシ URL を取得する
アプリをローカルで開発するために、Webhook プロキシ URL を使用して、GitHub からお使いのコンピューターまたは codespace に Webhook を転送することができます。 このチュートリアルでは、Smee.io を使用して Webhook プロキシ URL を指定し、イベントを転送します。
-
ターミナルで、次のコマンドを実行して Smee クライアントをインストールします。
Shell npm install --global smee-client
npm install --global smee-client
-
ブラウザーで https://smee.io/ にアクセスします。
-
[Start a new channel] (新しいチャネルの開始) をクリックします。
-
[Webhook Proxy URL] (Webhook プロキシ URL) の下にある完全な URL をコピーします。
-
ターミナルで、次のコマンドを実行して Smee クライアントを起動します。
YOUR_DOMAIN
を、前のステップでコピーした Webhook プロキシ URL に置き換えます。Shell smee --url YOUR_DOMAIN --path /event_handler --port 3000
smee --url YOUR_DOMAIN --path /event_handler --port 3000
次のような出力結果が表示されます。
Forwarding https://smee.io/YOUR_DOMAIN to http://127.0.0.1:3000/event_handler Connected https://smee.io/YOUR_DOMAIN
smee --url https://smee.io/YOUR_DOMAIN
コマンドは、Smee チャンネルによって受信されるすべての Webhook イベントをコンピューター上で稼働している Smee クライアントに転送するように Smee に指示します。 --path /event_handler
オプションは、イベントを /event_handler
ルートに転送します。 --port 3000
オプションは、ポート 3000 を指定します。これは、このチュートリアルで後ほどコードを追加するときに、リッスンするようにサーバーに指示するポートです。 Smee を使用すると、GitHub からの webhook を受信するために、マシンがパブリックなインターネットに対してオープンになっている必要はありません。 また、ブラウザでSmeeのURLを開いて、受信したwebhookのペイロードを調べることもできます。
このターミナル ウィンドウは開いたままにしておき、このガイドの残りの手順を完了するまでの間、Smee に接続したままにしておくことをお勧めします。 一意のドメインを失うことなく Smee クライアントを切断し、再接続することはできますが、接続したままにしておいて、別のターミナル ウィンドウで他のコマンドライン タスクを実行する方が簡単な場合もあります。
GitHub App を登録する
このチュートリアルでは、次の GitHub App を登録する必要があります。
- Webhook がアクティブになっている
- ローカルで受信できる Webhook URL を使用する
- リポジトリの "チェック" アクセス許可がある
- "チェック スイート" および "チェック実行" Webhook イベントにサブスクライブしている
以下の手順では、これらの設定で GitHub App を構成する方法について説明します。 GitHub App の設定について詳しくは、「GitHub App の登録」をご覧ください。
- GitHub の任意のページの右上隅にある、自分のプロファイル写真をクリックします。
- アカウント設定にアクセスしてください。
- 個人用アカウントが所有するアプリの場合は、[設定] をクリックします。
- 組織が所有するアプリの場合:
- [自分の組織] をクリックします。
- 組織の右側にある [設定] をクリックします。
- 左側のサイドバーで [ 開発者設定] をクリックします。
- 左側のサイドバーで、 [GitHub Apps] をクリックします。
- [新しい GitHub アプリ] をクリックします。
- [GitHub アプリ名] に、アプリの名前を入力します。 たとえば、
USERNAME-ci-test-app
です。USERNAME
はご自身の GitHub ユーザー名です。 - [ホームページの URL] の下に、アプリの URL を入力します。 たとえば、アプリのコードを格納するために作成したリポジトリの URL を使用できます。
- このチュートリアルでは、[ユーザーの特定と認可] と [インストール後] のセクションはスキップします。
- [Webhook] の下で [アクティブ] が選択されていることを確認します。
- [Webhook URL] の下に、前の Webhook プロキシ URL を入力します。 詳しくは、「Webhook プロキシ URL を取得する」を参照してください。
- [Webhook シークレット] に、ランダムな文字列を入力します。 このシークレットは、Webhook が GitHub によって送信されることを確認するために使用されます。 この文字列を保存します (後で使用します)。
- [リポジトリのアクセス許可] で、[チェック] の横にある [読み取りおよび書き込み] を選択します。
- [イベントへのサブスクライブ] の下で、 [チェック スイート] と [チェック実行] を選択します。
- [この GitHub アプリをインストールできる場所] で、 [このアカウントのみ] を選択します。 アプリを発行したい場合は、後でこれを変更できます。
- [GitHub アプリの作成] をクリックします。
アプリの識別情報と資格情報を格納する
このチュートリアルでは、アプリの資格情報と識別情報を環境変数として .env
ファイルに格納する方法を示します。 アプリをデプロイするときに、資格情報の保存方法を変更する必要があります。 詳しくは、「アプリのデプロイ」を参照してください。
資格情報をローカルに格納するため、以下の手順を実行する前に、セキュリティで保護されたコンピューターで作業していることを確認してください。
-
ターミナルで、クローンが格納されているディレクトリに移動します。
-
このディレクトリの最上位レベルに
.env
という名前のファイルを作成します。 -
.gitignore
ファイルに.env
を追加します。 こうすることで、アプリの資格情報を誤ってコミットするのを防ぐことができます。 -
次の内容を
.env
ファイルに追加します。 値は後の手順で更新します。Shell GITHUB_APP_IDENTIFIER="YOUR_APP_ID" GITHUB_WEBHOOK_SECRET="YOUR_WEBHOOK_SECRET" GITHUB_PRIVATE_KEY="YOUR_PRIVATE_KEY"
GITHUB_APP_IDENTIFIER="YOUR_APP_ID" GITHUB_WEBHOOK_SECRET="YOUR_WEBHOOK_SECRET" GITHUB_PRIVATE_KEY="YOUR_PRIVATE_KEY"
-
アプリの設定ページに移動します。
-
GitHub の任意のページの右上隅にある、自分のプロファイル写真をクリックします。
-
アカウント設定にアクセスしてください。
- 個人用アカウントが所有するアプリの場合は、[設定] をクリックします。
- 組織が所有するアプリの場合:
- [自分の組織] をクリックします。
- 組織の右側にある [設定] をクリックします。
-
左側のサイドバーで [ 開発者設定] をクリックします。
-
左側のサイドバーで、 [GitHub Apps] をクリックします。
-
アプリの名前の横にある [編集] をクリックします。
-
-
アプリの設定ページで、[アプリ ID] の横にある自分のアプリのアプリ ID を確認します。
-
.env
ファイルで、YOUR_APP_ID
を自分のアプリのアプリ ID に置き換えます。 -
.env
ファイルで、YOUR_WEBHOOK_SECRET
をアプリの Webhook シークレットに置き換えます。 Webhook シークレットを忘れた場合は、[Webhook シークレット (省略可能)] の下の [シークレットの変更] をクリックします。 新しいシークレットを入力し、 [変更の保存] をクリックします。 -
アプリの設定ページの [秘密キー] で、 [秘密キーの生成] をクリックします。 コンピューターにダウンロードされた秘密キー (
.pem
) ファイルが表示されます。 -
テキスト エディターで
.pem
ファイルを開くか、コマンド ラインでコマンドcat PATH/TO/YOUR/private-key.pem
を使用して、ファイルの内容を表示します。 -
ファイルの内容全体をコピーして、
.env
ファイルにGITHUB_PRIVATE_KEY
の値として貼り付け、値全体を二重引用符で囲みます。.env ファイルの例を次に示します。
GITHUB_APP_IDENTIFIER=12345 GITHUB_WEBHOOK_SECRET=your webhook secret GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- ... HkVN9... ... -----END RSA PRIVATE KEY-----"
GitHub App のコードを追加する
このセクションでは、GitHub App の基本的なテンプレート コードの一部を追加する方法を示し、コードの実行内容を説明します。 このチュートリアルの後半では、このコードを変更する方法、このコードに追加する方法、アプリの機能を構築する方法について説明します。
次のテンプレート コードを server.rb
ファイルに追加します。
require 'sinatra/base' # Use the Sinatra web framework require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API require 'dotenv/load' # Manages environment variables require 'json' # Allows your app to manipulate JSON data require 'openssl' # Verifies the webhook signature require 'jwt' # Authenticates a GitHub App require 'time' # Gets ISO 8601 representation of a Time object require 'logger' # Logs debug statements # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. class GHAapp < Sinatra::Application # Sets the port that's used when starting the web server. set :port, 3000 set :bind, '0.0.0.0' # Expects the private key in PEM format. Converts the newlines. PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a webhook secret. # The secret is used to verify that webhooks are sent by GitHub. WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET'] # The GitHub App's identifier (type integer). APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER'] # Turn on Sinatra's verbose logging during development configure :development do set :logging, Logger::DEBUG end # Executed before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) verify_webhook_signature # 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 authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do # ADD EVENT HANDLING HERE # 200 # success status end helpers do # ADD CREATE_CHECK_RUN HELPER METHOD HERE # # ADD INITIATE_CHECK_RUN HELPER METHOD HERE # # ADD CLONE_REPOSITORY HELPER METHOD HERE # # ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE # # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) # request.body is an IO or StringIO object # Rewind in case someone already read it request.body.rewind # The raw text of the body is required for webhook signature verification @payload_raw = request.body.read begin @payload = JSON.parse @payload_raw rescue => e fail 'Invalid JSON (#{e}): #{@payload_raw}' end end # Instantiate an Octokit client authenticated as a GitHub App. # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, # so GitHub can be sure that it came from the app and not altered by # a malicious third party. def authenticate_app payload = { # The time that this JWT was issued, _i.e._ now. iat: Time.now.to_i, # JWT expiration time (10 minute maximum) exp: Time.now.to_i + (10 * 60), # Your GitHub App's identifier number iss: APP_IDENTIFIER } # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end # Instantiate an Octokit client, authenticated as an installation of a # GitHub App, to run API operations. def authenticate_installation(payload) @installation_id = payload['installation']['id'] @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] @installation_client = Octokit::Client.new(bearer_token: @installation_token) end # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to # create the hash signature sent in the `X-HUB-Signature` header of each # webhook. This code computes the expected hash signature and compares it to # the signature sent in the `X-HUB-Signature` header. If they don't match, # this request is an attack, and you should reject it. GitHub uses the HMAC # hexdigest to compute the signature. The `X-HUB-Signature` looks something # like this: 'sha1=123456'. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw) halt 401 unless their_digest == our_digest # The X-GITHUB-EVENT header provides the name of the event. # The action value indicates the which action triggered the event. logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? end end # Finally some logic to let us run this server directly from the command line, # or with Rack. Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file # If they are the same—that is, we are running this file directly, call the # Sinatra run method run! if __FILE__ == $0 end
require 'sinatra/base' # Use the Sinatra web framework
require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API
require 'dotenv/load' # Manages environment variables
require 'json' # Allows your app to manipulate JSON data
require 'openssl' # Verifies the webhook signature
require 'jwt' # Authenticates a GitHub App
require 'time' # Gets ISO 8601 representation of a Time object
require 'logger' # Logs debug statements
# This code is a Sinatra app, for two reasons:
# 1. Because the app will require a landing page for installation.
# 2. To easily handle webhook events.
class GHAapp < Sinatra::Application
# Sets the port that's used when starting the web server.
set :port, 3000
set :bind, '0.0.0.0'
# Expects the private key in PEM format. Converts the newlines.
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))
# Your registered app must have a webhook secret.
# The secret is used to verify that webhooks are sent by GitHub.
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
# The GitHub App's identifier (type integer).
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
# Executed before each request to the `/event_handler` route
before '/event_handler' do
get_payload_request(request)
verify_webhook_signature
# 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
authenticate_app
# Authenticate the app installation in order to run API operations
authenticate_installation(@payload)
end
post '/event_handler' do
# ADD EVENT HANDLING HERE #
200 # success status
end
helpers do
# ADD CREATE_CHECK_RUN HELPER METHOD HERE #
# ADD INITIATE_CHECK_RUN HELPER METHOD HERE #
# ADD CLONE_REPOSITORY HELPER METHOD HERE #
# ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #
# Saves the raw payload and converts the payload to JSON format
def get_payload_request(request)
# request.body is an IO or StringIO object
# Rewind in case someone already read it
request.body.rewind
# The raw text of the body is required for webhook signature verification
@payload_raw = request.body.read
begin
@payload = JSON.parse @payload_raw
rescue => e
fail 'Invalid JSON (#{e}): #{@payload_raw}'
end
end
# Instantiate an Octokit client authenticated as a GitHub App.
# GitHub App authentication requires that you construct a
# JWT (https://jwt.io/introduction/) signed with the app's private key,
# so GitHub can be sure that it came from the app and not altered by
# a malicious third party.
def authenticate_app
payload = {
# The time that this JWT was issued, _i.e._ now.
iat: Time.now.to_i,
# JWT expiration time (10 minute maximum)
exp: Time.now.to_i + (10 * 60),
# Your GitHub App's identifier number
iss: APP_IDENTIFIER
}
# Cryptographically sign the JWT.
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
# Create the Octokit client, using the JWT as the auth token.
@app_client ||= Octokit::Client.new(bearer_token: jwt)
end
# Instantiate an Octokit client, authenticated as an installation of a
# GitHub App, to run API operations.
def authenticate_installation(payload)
@installation_id = payload['installation']['id']
@installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token]
@installation_client = Octokit::Client.new(bearer_token: @installation_token)
end
# Check X-Hub-Signature to confirm that this webhook was generated by
# GitHub, and not a malicious third party.
#
# GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to
# create the hash signature sent in the `X-HUB-Signature` header of each
# webhook. This code computes the expected hash signature and compares it to
# the signature sent in the `X-HUB-Signature` header. If they don't match,
# this request is an attack, and you should reject it. GitHub uses the HMAC
# hexdigest to compute the signature. The `X-HUB-Signature` looks something
# like this: 'sha1=123456'.
def verify_webhook_signature
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
method, their_digest = their_signature_header.split('=')
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
halt 401 unless their_digest == our_digest
# The X-GITHUB-EVENT header provides the name of the event.
# The action value indicates the which action triggered the event.
logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil?
end
end
# Finally some logic to let us run this server directly from the command line,
# or with Rack. Don't worry too much about this code. But, for the curious:
# $0 is the executed file
# __FILE__ is the current file
# If they are the same—that is, we are running this file directly, call the
# Sinatra run method
run! if __FILE__ == $0
end
このセクションの残りの部分では、テンプレート コードの実行内容について説明します。 このセクションで完了しなければならないステップはありません。 テンプレート コードについて既にご存じの場合は、手順をスキップして「サーバーを起動する」に進んでください。
テンプレート コードについて
テキスト エディターで server.rb
ファイルを開きます。 このファイル全体に、テンプレート コードの追加コンテキストを提供するコメントがあります。 これらのコメントを注意深く読んで、さらには作成する新しいコードに添えて独自のコメントを追加することをおすすめします。
必要なファイルの一覧の下に、最初に表示されるコードは、class GHApp < Sinatra::Application
宣言です。 このクラス内に、GitHub App のすべてのコードを記述します。 以下のセクションでは、このクラス内のコードの実行内容について詳しく説明します。
ポートを設定する
class GHApp < Sinatra::Application
宣言で最初に目にするのは、set :port 3000
です。 これは、Web サーバーの起動時に使用されるポートを設定します。これは、「Webhook プロキシ URL を取得する」で Webhook ペイロードをリダイレクトしたポートと一致します。
# Sets the port that's used when starting the web server.
set :port, 3000
set :bind, '0.0.0.0'
環境変数の読み取り
次に、このクラスでは、「アプリの識別情報と資格情報を格納する」で設定した 3 つの環境変数を読み取り、後で使用するために変数に格納します。
# Expects the private key in PEM format. Converts the newlines.
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))
# Your registered app must have a webhook secret.
# The secret is used to verify that webhooks are sent by GitHub.
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
# The GitHub App's identifier (type integer).
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
ログ記録を有効にする
次は、Sinatraにおけるデフォルトの環境である開発の間、ロギングを有効にするコードブロックです。 このコードは、DEBUG
レベルでのログを有効にします。これにより、アプリの開発中、有用な出力がターミナルに表示されます。
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
before
フィルターを定義する
Sinatra では、ルート ハンドラーの前にコードを実行できるbefore
フィルターが使用されます。 テンプレートの before
ブロックは、4 つのヘルパー メソッド (get_payload_request
、verify_webhook_signature
、authenticate_app
、authenticate_installation
) を呼び出します。 詳しい情報については、Sinatra のドキュメント「フィルター」および「ヘルパー」を参照してください。
# Executed before each request to the `/event_handler` route
before '/event_handler' do
get_payload_request(request)
verify_webhook_signature
# 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
authenticate_app
# Authenticate the app installation in order to run API operations
authenticate_installation(@payload)
end
これらのヘルパー メソッドはそれぞれ、コードの後半 (helpers do
で始まるコード ブロック) で定義されます。 詳しい情報については、「ヘルパー メソッドを定義する」を参照してください。
verify_webhook_signature
の下の unless @payload
で始まるコードは、セキュリティ対策です。 Webhook ペイロードでリポジトリ名が指定されている場合、このコードは、リポジトリ名に英字、ハイフン、アンダースコアのみが含まれていることを検証します。 これは、悪意のあるアクターが任意のコマンドを実行したり、リポジトリの偽名を挿入したりしようとするのを確実に防ぐのに役立ちます。 後ほど、helpers do
で始まるコード ブロックで、verify_webhook_signature
ヘルパー メソッドにより、追加のセキュリティ対策として、受信する Webhook ペイロードも検証されます。
ルートハンドラの定義
テンプレートコードには、空のルートが含まれています。 このコードは、/event_handler
ルートへのすべての POST
要求を処理します。 後で、これにさらにコードを追加します。
post '/event_handler' do
end
ヘルパー メソッドを定義する
テンプレート コードの before
ブロックでは、4 つのヘルパー メソッドが呼び出されます。 helpers do
コード ブロックでは、これらの各ヘルパー メソッドを定義します。
webhookペイロードの処理
最初のヘルパー メソッド get_payload_request
は、Webhook ペイロードをキャプチャし、JSON 形式に変換します。これにより、ペイロードのデータにはるかに簡単にアクセスできるようになります。
webhookの署名の検証
2 番目のメソッド verify_webhook_signature
は Webhook シグネチャの検証を実行して、GitHub でこのイベントが生成されたことを確認します。 verify_webhook_signature
ヘルパー メソッドのコードについて詳しくは、「Webhook 配信を検証する」をご覧ください。 Webhook がセキュリティで保護されている場合、このメソッドはすべての受信ペイロードをターミナルにログします。 ロガー コードは、Web サーバーが動作していることを確認するのに役立ちます。
GitHub App として認証を行う
3 番目のヘルパー メソッド authenticate_app
を使用すると、GitHub App で認証を行うことができるため、インストール トークンを要求できます。
API 呼び出しを行うには、Octokit ライブラリを使用します。 このライブラリで何か興味深いことを行うには、GitHub App で認証を行う必要があります。 Octokit ライブラリについて詳しくは、Octokit のドキュメントを参照してください。
GitHub Apps には、次の 3 つの認証方法があります。
- JSON Web Token (JWT) を使用して、GitHub App として認証を行う。
- インストール アクセス トークンを使用して、GitHub App の特定のインストールとして認証を行う。
- ユーザーに代わって認証を行う。 このチュートリアルでは、この認証方法は使用しません。
インストールとしての認証については、次のセクション「インストールとして認証を行う」で説明します。
GitHub App として認証を行うと、以下のことが可能になります。
- GitHub App について管理情報の概要を取得できます。
- アプリケーションのインストールのため、アクセストークンをリクエストできます。
たとえば、GitHub App として認証を行うと、アプリをインストールしたアカウント (Organization および個人) の一覧を取得できます。 しかし、この認証方法ではAPIを使って多くのことは行えません。 インストールの代わりにリポジトリのデータにアクセスして操作を行うには、インストールとして認証を受けなければなりません。 そのためには、まず GitHub App として認証を行って、インストール アクセス トークンを要求する必要があります。 詳しくは、「GitHub アプリでの認証について」を参照してください。
Octokit.rb ライブラリを使用して API 呼び出しを行う前に、authenticate_app
ヘルパー メソッドを使用して、GitHub App として認証された Octokit クライアントを初期化する必要があります。
# Instantiate an Octokit client authenticated as a GitHub App.
# GitHub App authentication requires that you construct a
# JWT (https://jwt.io/introduction/) signed with the app's private key,
# so GitHub can be sure that it came from the app an not altered by
# a malicious third party.
def authenticate_app
payload = {
# The time that this JWT was issued, _i.e._ now.
iat: Time.now.to_i,
# JWT expiration time (10 minute maximum)
exp: Time.now.to_i + (10 * 60),
# Your GitHub App's identifier number
iss: APP_IDENTIFIER
}
# Cryptographically sign the JWT
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
# Create the Octokit client, using the JWT as the auth token.
@app_client ||= Octokit::Client.new(bearer_token: jwt)
end
上記のコードは JSON Web トークン (JWT) を生成し、これを (アプリの秘密キーと共に) 使用して Octokit クライアントを初期化します。 GitHubは、保存されたアプリケーションの公開鍵でトークンを検証することによって、リクエストの認証を確認します。 このコードのしくみについて詳しくは、「GitHub アプリの JSON Web トークン (JWT) の生成」をご覧ください。
インストールとして認証を行う
4 番目で最後のヘルパー メソッド authenticate_installation
は、インストールとして認証された Octokit クライアント を初期化します。これを使用して、API に対して認証された呼び出しを行うことができます。
インストール とは、アプリをインストールしたユーザーまたは組織のアカウントを指します。 アプリに対して、そのアカウント上の複数のリポジトリへのアクセスが許可された場合でも、それは同じアカウント内にあるため、1 つのインストールとしてのみカウントされます。
# Instantiate an Octokit client authenticated as an installation of a
# GitHub App to run API operations.
def authenticate_installation(payload)
installation_id = payload['installation']['id']
installation_token = @app_client.create_app_installation_access_token(installation_id)[:token]
@installation_client = Octokit::Client.new(bearer_token: installation_token)
end
create_app_installation_access_token
Octokit メソッドはインストール トークンを作成します。 詳しい情報については、Octokit のドキュメントの「create_installation_access_token」を参照してください。
このメソッドは、2つの引数を取ります。
- Installation (整数): GitHub App インストールの ID
- オプション (ハッシュ、既定値
{}
): カスタマイズ可能な一連のオプション
GitHub App で Webhook が受信されるたびに、id
を含む installation
オブジェクトが含まれます。 GitHub App として認証されたクライアントを使用して、この ID を create_app_installation_access_token
メソッドに渡して、インストールごとにアクセス トークンを生成します。 メソッドにはオプションを渡していないので、オプションはデフォルトの空のハッシュになります。 create_app_installation_access_token
の応答には、token
と expired_at
の 2 つのフィールドが含まれています。 テンプレートのコードはレスポンス中のトークンを選択し、インストールクライアントを初期化します。
このメソッドを利用して、アプリケーションは新しいwebhookのペイロードを受信するたびに、そのイベントをトリガーしたインストールのためのクライアントを作成します。 この認証プロセスによって、GitHub App は任意のアカウントのすべてのインストールで動作できます。
サーバーを起動する
このアプリケーションではまだ何も 実行 できませんが、この時点でサーバー上で稼働させることができます。
-
ターミナルで、Smee がまだ実行されていることを確認します。 詳しくは、「Webhook プロキシ URL を取得する」を参照してください。
-
ターミナルで新しいタブを開き、
cd
を実行して、このチュートリアルで前に作成したリポジトリの複製先であるディレクトリに変更します。 詳しい情報については、「GitHub App のコードを格納するリポジトリを作成する」を参照してください。 このリポジトリの Ruby コードにより、Sinatra Web サーバーが起動されます。 -
次の 2 つのコマンドを 1 つずつ実行して、依存関係をインストールします。
Shell gem install bundler
gem install bundler
Shell bundle install
bundle install
-
依存関係をインストールしたら、次のコマンドを実行してサーバーを起動します。
Shell bundle exec ruby server.rb
bundle exec ruby server.rb
次のように、応答が表示されます。
> == Sinatra (v2.2.3) has taken the stage on 3000 for development with backup from Puma > Puma starting in single mode... > * Puma version: 6.3.0 (ruby 3.1.2-p20) ("Mugi No Toki Itaru") > * Min threads: 0 > * Max threads: 5 > * Environment: development > * PID: 14915 > * Listening on http://0.0.0.0:3000 > Use Ctrl-C to stop
エラーが表示された場合は、
server.rb
を含むディレクトリに.env
ファイルが作成されていることを確認します。 -
サーバーをテストするには、ブラウザーで
http://localhost:3000
に移動します。"Sinatra はこの歌を知りません" というエラー ページが表示された場合、アプリは期待どおりに動作しています。 これはエラー ページですが、Sinatra のエラー ページであるため、期待どおりにアプリケーションがサーバーに接続されています。 このメッセージが表示されているのは、他に表示するものを何もアプリケーションに加えていないからです。
サーバーがアプリをリッスンしているかどうかをテストする
サーバーがアプリケーションを待ち受けているかは、受信するイベントをトリガーすればテストできます。 これを行うには、テスト リポジトリにアプリをインストールします。これにより、installation
イベントがアプリに送信されます。 アプリでイベントが受信されると、server.rb
を実行しているターミナル タブに出力が表示されます。
-
チュートリアル コードのテストに使用する新しいリポジトリを作成します。 詳しくは、「新しいリポジトリの作成」を参照してください。
-
先ほど作成したリポジトリに GitHub App をインストールします。 詳しくは、「独自の GitHub App のインストール」を参照してください。 インストール プロセス中に、 [リボジトリの選択のみ] を選択し、前の手順で作成したリポジトリを選択します。
-
[インストール] をクリックすると、
server.rb
を実行しているターミナル タブで出力を確認します。 次のような結果が表示されます。> D, [2023-06-08T15:45:43.773077 #30488] DEBUG -- : ---- received event installation > D, [2023-06-08T15:45:43.773141 #30488]] DEBUG -- : ---- action created > 192.30.252.44 - - [08/Jun/2023:15:45:43 -0400] "POST /event_handler HTTP/1.1" 200 - 0.5390
このような出力が表示される場合は、GitHub アカウントにインストールされたことを示す通知をアプリが受信したことを意味します。 アプリは、サーバー上で期待どおりに実行されています。
この出力が表示されない場合は、Smee が別のターミナル タブで正しく実行されることを確認します。Smee を再起動する必要がある場合は、アプリを "アンインストール" して "再インストール" することで、
installation
イベントをアプリにもう一度送信し、ターミナルで出力を確認する必要もあることに注意してください。__ __
上記のターミナル出力の出力元が不明な場合は、「GitHub App のコードを追加する」で server.rb
に追加したアプリ テンプレート コードに記述されています。
第 1 部 Checks API インターフェースを作成する
このパートでは、check_suite
Webhook イベントの受信と、チェック実行の作成および更新に必要なコードを追加します。 また、GitHub でチェックが再要求された場合にチェック実行を作成する方法についても説明します。 このセクションを終了すると、GitHub pull request に作成されたチェック実行を表示できるようになります。
このセクションで作成するチェック実行は、コードのチェックを実行しません。 その機能は、「パート 2: CI テストの作成」で追加します。
ローカルサーバーにwebhook ペイロードを転送するよう Smee チャンネルが構成されているでしょうか。 サーバーが実行されており、テスト リポジトリに登録してインストールした GitHub App に接続されている必要があります。
パート 1 では、以下のステップを完了させます。
ステップ 1.1. イベント処理の追加
アプリはチェック スイートとチェック実行イベントにサブスクライブされているので、check_suite
および check_run
Webhook の受信を開始します。 GitHub は、Webhook ペイロードを POST
要求として送信します。 Smee Webhook ペイロードを http://localhost:3000/event_handler
に転送したため、サーバーは post '/event_handler'
ルートで POST
要求のペイロードを受信します。
「GitHub App のコードを追加する」で作成した server.rb
ファイルを開き、次のコードを見つけます。 テンプレート コードには、空の post '/event_handler'
ルートが既に含まれています。 空のルートは次のようになっています。
post '/event_handler' do
# ADD EVENT HANDLING HERE #
200 # success status
end
post '/event_handler' do
で始まるコード ブロックの # ADD EVENT HANDLING HERE #
と書かれている箇所に、次のコードを追加します。 このルートは 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 # ADD CHECK_RUN METHOD HERE # end
# 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
# ADD CHECK_RUN METHOD HERE #
end
GitHub から送信されるすべてのイベントには、HTTP_X_GITHUB_EVENT
という要求ヘッダーが含まれています。これは、POST
要求のイベントの種類を示します。 ここで関心のあるイベントの種類は check_suite
だけであり、これは新しいチェック スイートが作成されると出力されます。 各イベントには、イベントをトリガーしたアクションの種類を示す追加の action
フィールドがあります。 check_suite
の場合、action
フィールドは requested
、rerequested
、または completed
になります。
requested
アクションはリポジトリにコードがプッシュされるたびにチェック実行を要求し、rerequested
アクションはリポジトリに既に存在するコードのチェックの再実行を要求します。 requested
と rerequested
のどちらのアクションでもチェック実行を作成する必要があるため、create_check_run
というヘルパーを呼び出します。 では、このメソッドを書いてみましょう。
ステップ 1.2. チェック実行の作成
他のルートでも使う場合に備えて、この新しいメソッドを Sinatra ヘルパーとして追加します。
helpers do
で始まるコード ブロックの # ADD CREATE_CHECK_RUN HELPER METHOD HERE #
と書かれている箇所に、次のコードを追加します。
# Create a new check run with status "queued" def create_check_run @installation_client.create_check_run( # [String, Integer, Hash, Octokit Repository object] A GitHub repository. @payload['repository']['full_name'], # [String] The name of your check run. 'Octo RuboCop', # [String] The SHA of the commit to check # The payload structure differs depending on whether a check run or a check suite event occurred. @payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha'], # [Hash] 'Accept' header option, to avoid a warning about the API not being ready for production use. accept: 'application/vnd.github+json' ) end
# Create a new check run with status "queued"
def create_check_run
@installation_client.create_check_run(
# [String, Integer, Hash, Octokit Repository object] A GitHub repository.
@payload['repository']['full_name'],
# [String] The name of your check run.
'Octo RuboCop',
# [String] The SHA of the commit to check
# The payload structure differs depending on whether a check run or a check suite event occurred.
@payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha'],
# [Hash] 'Accept' header option, to avoid a warning about the API not being ready for production use.
accept: 'application/vnd.github+json'
)
end
このコードでは、Octokit create_check_run メソッドを使用して POST /repos/{owner}/{repo}/check-runs
エンドポイントを呼び出します。 このエンドポイントの詳しい情報については、「チェック実行用 REST API エンドポイント」を参照してください。
チェック実行を作成するために必要な入力パラメーターは、name
と head_sha
の 2 つのみです。 このチュートリアルで後ほど RuboCop を使用して CI テストを実装するため、このコードでは、実行チェックに "Octo RuboCop" という名前を付けます。 ただし、チェック実行には任意の名前を自由に選択できます。 RuboCop について詳しくは、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
オブジェクトから読み取ります。
コードのテスト
次の手順は、コードが動作し、新しいチェック実行が正常に作成されたことをテストする方法を示します。
-
次のコマンドを実行して、ターミナルからサーバーを再起動します。 サーバーが既に実行されている場合は、まずターミナルに「
Ctrl-C
」と入力してサーバーを停止し、次のコマンドを実行してサーバーを再度起動します。Shell ruby server.rb
ruby server.rb
-
「サーバーがアプリをリッスンしているかどうかをテストする」で作成したテスト リポジトリに pull request を作成します。 これは、アプリにアクセス権が付与されたリポジトリです。
-
先ほど作成した pull request で、 [チェック] タブに移動します。"Octo RuboCop" という名前、または先ほどチェック実行に選択した名前のチェック実行が表示されます。
[チェック] タブに他のアプリが表示される場合は、チェックに対する読み取りと書き込みアクセス権を持ち、チェック スイートおよびチェック実行イベントにサブスクライブしている他のアプリが、リポジトリにインストールされていることを意味します。 また、リポジトリ上に、pull_request
または pull_request_target
イベントによってトリガーされる GitHub Actions ワークフローがあることを意味する場合もあります。
ここまでで、GitHub に、チェック実行を作成するように指示しました。 pull request のチェック実行の状態は、queued に設定され、黄色のアイコンが表示されます。 次の手順では、GitHub で実行チェックが作成され、その状態が更新されるまで待ちます。
ステップ 1.3. チェック実行の更新
create_check_run
メソッドを実行すると、GitHub に新しいチェック実行を作成するように要求されます。 GitHub でチェック実行の作成が完了すると、created
アクションを含む check_run
Webhook イベントを受け取ります。 このイベントは、チェックの実行を開始する合図です。
created
アクションを見つけるようにイベント ハンドラーを更新します。 イベント ハンドラーを更新するときに、rerequested
アクションに対する条件を追加できます。 [再実行] ボタンをクリックして GitHub で 1 つのテストが再実行されると、GitHub により、rerequested
チェック実行イベントがアプリに送信されます。 チェック実行が rerequested
の場合、プロセスを最初からやり直して、新しいチェック実行を作成します。 そのためには、post '/event_handler'
ルートに check_run
イベントの条件を含めます。
post '/event_handler' do
で始まるコード ブロックの # ADD CHECK_RUN METHOD HERE #
と書かれている箇所に、次のコードを追加します。
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 # ADD REQUESTED_ACTION METHOD HERE # end end
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
# ADD REQUESTED_ACTION METHOD HERE #
end
end
GitHub では、created
チェック実行に関するすべてのイベントを、リポジトリにインストールされていて必要なチェック アクセス許可を持つすべてのアプリに送信します。 これはつまり、あなたのアプリケーションが他のアプリケーションにより作成されたチェック実行を受信するということです。 created
チェック実行は、チェックの実行を要求されているアプリのみに GitHub が送信する requested
や rerequested
チェック スイートとは多少異なります。 上記のコードは、チェック実行のアプリケーション ID を待ち受けます。 リポジトリの他のアプリケーションに対するチェック実行はすべて遮断されます。
次に、initiate_check_run
メソッドを作成します。そこでは、チェック実行のステータスを更新して、CI テストの開始を準備します。
このセクションでは、CI テストはまだ開始しませんが、チェック実行のステータスを queued
から pending
に更新し、さらに pending
から completed
に更新する手順を調べることで、チェック実行のフロー全体を確認します。 「パート 2: CI テストの作成」では、CI テストを実際に実行するコードを追加します。
initiate_check_run
メソッドを作成し、チェック実行のステータスを更新しましょう。
helpers do
で始まるコード ブロックの # ADD INITIATE_CHECK_RUN HELPER METHOD HERE #
と書かれている箇所に、次のコードを追加します。
# 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. @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'in_progress', accept: 'application/vnd.github+json' ) # ***** RUN A CI TEST ***** # Mark the check run as complete! @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'completed', conclusion: 'success', accept: 'application/vnd.github+json' ) end
# 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.
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'in_progress',
accept: 'application/vnd.github+json'
)
# ***** RUN A CI TEST *****
# Mark the check run as complete!
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'completed',
conclusion: 'success',
accept: 'application/vnd.github+json'
)
end
上記のコードでは、update_check_run
Octokit メソッドを使用して PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
エンドポイントを呼び出し、既に作成したチェック実行を更新します。 このエンドポイントの詳しい情報については、「チェック実行用 REST API エンドポイント」を参照してください。
このコードがしていることを説明しましょう。 まず、チェック実行のステータスを in_progress
に更新し、started_at
の日時を現在の日時に暗黙的に設定します。 このチュートリアルのパート 2 では、実際の CI テストを開始するコードを ***** RUN A CI TEST *****
の下に追加します。 今はこのセクションをプレースホルダーとして残しておきましょう。そうすると、続くコードが CI のプロセスを成功させ、すべてのテストに合格したことをシミュレートすることになります。 最後に、コードでチェック実行のステータスを再び completed
に更新します。
REST API を使用してチェック実行の状態 completed
を指定する場合、conclusion
および completed_at
パラメーターが必要です。 conclusion
はチェック実行の結果の要約であり、success
、failure
、neutral
、cancelled
、timed_out
、skipped
、または action_required
を指定できます。 conclusion は success
に、completed_at
の日時は現在の日時に、ステータスは completed
に設定します。
チェックが行っていることについてより詳しく指定することもできますが、それは次のセクションで行うことにします。
コードのテスト
次の手順は、コードが機能し、作成した新しい [すべて再実行] ボタンが機能することをテストする方法を示します。
-
次のコマンドを実行して、ターミナルからサーバーを再起動します。 サーバーが既に実行されている場合は、まずターミナルに「
Ctrl-C
」と入力してサーバーを停止し、次のコマンドを実行してサーバーを再度起動します。Shell ruby server.rb
ruby server.rb
-
「サーバーがアプリをリッスンしているかどうかをテストする」で作成したテスト リポジトリに pull request を作成します。 これは、アプリにアクセス権が付与されたリポジトリです。
-
先ほど作成した pull request で、 [チェック] タブに移動します。[すべて再実行] ボタンが表示されます。
-
右上隅にある [すべて実行] ボタンをクリックします。 テストが再度実行され、
success
で終了します。
第 2 部 CI テストの作成
API イベントを受信し、チェック実行を作成するためのインターフェースが完成したので、CI テストを実装するチェック実行を作成できます。
RuboCop は Ruby コードのリンターおよびフォーマッタです。 これは、Ruby コードが Ruby スタイル ガイドに準拠していることを確認します。 詳しい情報については、RuboCop のドキュメントを参照してください。
RuboCop の主な機能は、以下の 3 つです。
- コードのスタイルを確認する文法チェック
- コードのフォーマット
ruby -w
を使って Ruby のネイティブ リンティング機能を置き換える
アプリは CI サーバー上で RuboCop を実行し、RuboCop から報告される結果を GitHub に報告するチェック実行 (この場合は CI テスト) を作成します。
REST API を使用すると、ステータス、画像、要約、注釈、HTTP 要求されたアクションなど、各チェック実行の詳細を報告できます。
アノテーションとは、リポジトリのコードの特定の行についての情報です。 アノテーションを使用すると、追加情報を表示したいコードの部分を細かく指定して、それを視覚化できます。 たとえば、その情報を特定のコード行に関するコメント、エラー、または警告として表示できます。 このチュートリアルでは、注釈を使用して RuboCop のエラーを視覚化します。
要求されたアクションを利用するため、アプリ開発者は pull request の [チェック] タブにボタンを作成できます。 これらのボタンのいずれかがクリックされると、クリックによって GitHub App に requested_action
check_run
イベントが送信されます。 アプリケーションが実行するアクションは、アプリケーション開発者が自由に設定できます。 このチュートリアルでは、ユーザーが RuboCop に対して、検出したエラーを修正するように要求するためのボタンを追加する方法について説明します。 RuboCop はコマンド ライン オプションを使ったエラーの自動修正をサポートしており、ここでは requested_action
を構成してこのオプションを利用します。
このセクションでは、以下のステップを完了させます。
- Ruby ファイルを追加する
- RuboCop がテスト リポジトリをクローンできるようにする
- RuboCop を実行する
- RuboCop のエラーを収集する
- チェック実行を CI テストの結果で更新する
- RuboCop のエラーを自動的に修正する
ステップ 2.1. Ruby ファイルを追加する
RuboCop がチェックするため、特定のファイルまたはディレクトリ全体を渡すことができます。 このチュートリアルでは、ディレクトリ全体で RuboCop を実行します。 RuboCop は Ruby コードのみをチェックします。 GitHub App をテストするには、RuboCop で検出されたエラーを含む Ruby ファイルをリポジトリに追加する必要があります。 次の Ruby ファイルをリポジトリに追加した後、コードで RuboCop を実行するように CI チェックを更新します。
-
「サーバーがアプリをリッスンしているかどうかをテストする」で作成したテスト リポジトリに移動します。 これは、アプリにアクセス権が付与されたリポジトリです。
-
myfile.rb
という名前で新しいファイルを作成します。 詳しくは、「新しいファイルの作成」を参照してください。 -
myfile.rb
に次の内容を追加します。Ruby # frozen_string_literal: true # 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
# frozen_string_literal: true # 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
-
ファイルをローカルで作成した場合は必ず、GitHub 上のリポジトリにファイルをコミットしてプッシュしてください。
ステップ 2.2. RuboCop でテスト リポジトリをクローンできるようにする
RuboCop はコマンドラインユーティリティとして使用できます。 つまり、リポジトリ上で RuboCop を実行する場合、RuboCop でファイルを解析できるように、GitHub App で CI サーバー上にリポジトリのローカル コピーをクローンする必要があります。 そのためには、コードで Git 操作を実行できる必要があり、GitHub App にはリポジトリをクローンするための適切な権限が必要です。
Git 操作を実行できるようにする
Ruby アプリで Git の操作を実行するには、ruby-git gem を使えます。 「セットアップ」で作成した Gemfile
には既に ruby-git gem が含まれており、「サーバーを起動する」で bundle install
を実行したときに、これをインストールしました。
次に、ファイルの上部にある他server.rb
のrequire
項目の下に、次のコードを追加します。
require 'git'
require 'git'
アプリのアクセス許可を更新する
次に、GitHub App のアクセス許可を更新する必要があります。 リポジトリをクローンするには、アプリに "コンテンツ" に対する読み取りアクセス許可が必要です。 このチュートリアルで後ほど、コンテンツを GitHub にプッシュするための書き込みアクセス許可が必要になります。 アプリケーションの権限を更新するには、以下の手順に従います。
- アプリの設定ページでアプリを選び、サイドバーの [アクセス許可とイベント] をクリックします。
- [リポジトリのアクセス許可] で、[コンテンツ] の横にある [読み取りと書き込み] を選択します。
- ページの下部にある [変更の保存] をクリックします。
- アプリケーションを自分のアカウントにインストールしたなら、メールをチェックして、新しい権限を受諾するリンクに従ってください。 アプリケーションの権限あるいはwebhookを変更した場合、そのアプリケーションをインストールしたユーザ(自分自身を含む)は、変更が有効になる前に新しい権限を承認しなければなりません。 インストール ページに移動することで、新しいアクセス許可を受け入れることもできます。 アプリが異なるアクセス許可を要求していることを知らせるリンクが、アプリ名の下に表示されます。 [要求の確認] をクリックしてから、[新しいアクセス許可を受け入れる] をクリックします。
リポジトリをクローンするコードを追加する
リポジトリをクローンするには、コードで GitHub App のアクセス許可と Octokit SDK を使用して、アプリのインストール トークン (x-access-token:TOKEN
) を作成し、それを次のクローン コマンドで使用します。
git clone https://x-access-token:TOKEN@github.com/OWNER/REPO.git
上記のコードは、HTTP 経由でリポジトリをクローンします。 コードには、リポジトリの所有者 (ユーザまたは Organization) およびリポジトリ名を含む、リポジトリのフルネームを入力する必要があります。 たとえば、octocat Hello-World リポジトリのフル ネームは octocat/hello-world
です。
server.rb
ファイルを開きます。 helpers do
で始まるコード ブロックの # ADD CLONE_REPOSITORY HELPER METHOD HERE #
と書かれている箇所に、次のコードを追加します。
# 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
# 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
上のコードでは、ruby-git
gem を使い、アプリのインストール トークンを使ってリポジトリをクローンしています。 コードは、server.rb
と同じディレクトリにクローンされます。 リポジトリで Git コマンドを実行するには、コードをリポジトリのディレクトリに変更する必要があります。 ディレクトリを変更する前に、コードで現在の作業ディレクトリを変数 (pwd
) に保存して戻る場所を記憶した後、clone_repository
メソッドを終了します。
このコードは、リポジトリ ディレクトリから、最新の変更をフェッチして統合し (@git.pull
)、特定の Git 参照をチェックアウト (@git.checkout(ref)
) します。 これらすべてを実行するコードは、独自のメソッドにうまく収まります。 メソッドがこれらの操作を実行するには、リポジトリの名前とフルネーム、チェックアウトする ref が必要です。 ref にはコミット SHA、ブランチ、タグ名を指定できます。 完了したら、コードによって、ディレクトリは元の作業ディレクトリ (pwd
) に戻されます。
これで、リポジトリをクローンして ref をチェックアウトするメソッドができました。次に、必要な入力パラメーターを取得して新しい clone_repository
メソッドを呼び出すコードを追加する必要があります。
helpers do
で始まるコード ブロックの initiate_check_run
ヘルパー メソッド内で、# ***** 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) # ADD CODE HERE TO RUN RUBOCOP #
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)
# ADD CODE HERE TO RUN RUBOCOP #
上のコードは、リポジトリのフル ネームとコミットのヘッド SHA を、check_run
Webhook ペイロードから取得します。
ステップ 2.3. RuboCop を実行する
ここまでのコードで、リポジトリがクローンされ、CI サーバーを使用してチェック実行が作成されます。 次に、RuboCop リンターとチェック注釈について詳しく説明します。
まず、RuboCop を実行し、スタイル コード エラーを JSON 形式で保存するコードを追加します。
helpers do
で始まるコード ブロックで、initiate_check_run
ヘルパー メソッドを見つけます。 そのヘルパー メソッド内の clone_repository(full_repo_name, repository, head_sha)
の下にある # ADD CODE HERE TO RUN RUBOCOP #
と書かれている箇所に、次のコードを追加します。
# Run RuboCop on all files in the repository @report = `rubocop '#{repository}' --format json` logger.debug @report `rm -rf #{repository}` @output = JSON.parse @report # ADD ANNOTATIONS CODE HERE #
# Run RuboCop on all files in the repository
@report = `rubocop '#{repository}' --format json`
logger.debug @report
`rm -rf #{repository}`
@output = JSON.parse @report
# ADD ANNOTATIONS CODE HERE #
上記のコードは、リポジトリのディレクトリにある全てのファイルで RuboCop を実行します。 オプション --format json
は、マシンで解析可能な形式でリンティング結果のコピーを保存します。 詳しい情報と JSON 形式の例については、RuboCop のドキュメントの「JSON フォーマッタ」を参照してください。このコードでは、JSON も解析されるため、@output
変数を使用して GitHub App 内のキーと値に簡単にアクセスできます。
RuboCop を実行し、リンティング結果を保存した後、このコードは、コマンド rm -rf
を実行して、リポジトリのチェックアウトを削除します。 コードでは、RuboCop の結果が @report
変数に格納されるため、リポジトリのチェックアウトを安全に削除できます。
rm -rf
コマンドは、元に戻すことができません。 アプリのセキュリティを維持するために、このチュートリアルのコードでは、受信 Webhook をチェックして、アプリで意図されているものとは異なるディレクトリを削除するために使用される可能性のある悪意のあるコマンドが挿入されていないかどうかを確認します。 たとえば、悪意のあるアクターが ./
というリポジトリ名を使って Webhook を送信した場合、アプリはルート ディレクトリを削除します。 verify_webhook_signature
メソッドは、Webhook の送信元を検証します。 verify_webhook_signature
イベント ハンドラーによって、リポジトリ名が有効であるかどうかも確認されます。 詳しい情報については、「before
フィルターを定義する」を参照してください。
コードのテスト
次の手順は、コードが機能することをテストし、RuboCop によって報告されたエラーを表示する方法を示します。
-
次のコマンドを実行して、ターミナルからサーバーを再起動します。 サーバーが既に実行されている場合は、まずターミナルに「
Ctrl-C
」と入力してサーバーを停止し、次のコマンドを実行してサーバーを再度起動します。Shell ruby server.rb
ruby server.rb
-
myfile.rb
ファイルを追加したリポジトリに、新しい pull request を作成します。 -
サーバーが実行されているターミナル タブに、リンティング エラーを含むデバッグ出力が表示されます。 リンティング エラーは、書式設定なしで出力されます。 デバッグ出力をコピーし、JSON フォーマッタなどの Web ツールに貼り付けて、JSON 出力を次の例のように書式設定することができます。
{ "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 エラーを収集する
@output
変数には、RuboCop レポートの解析済みの JSON の結果が含まれます。 前の手順の出力例で示しているように、結果には summary
セクションが含まれています。コードでは、これを使用して、エラーがあるかどうかをすばやく判断することができます。 次のコードは、エラーが報告されないときは、チェック実行の結果を success
に設定します。 RuboCop は files
配列で各ファイルのエラーを報告するので、エラーがある場合、file オブジェクトからデータを抽出する必要があります。
チェック実行を管理する REST API エンドポイントを使用すると、特定のコード行の注釈を作成できます。 チェック実行を作成または更新する際に、アノテーションを追加できます。 このチュートリアルでは、PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
エンドポイントを使用して、実行チェックを注釈で更新します。 このエンドポイントの詳しい情報については、「チェック実行用 REST API エンドポイント」を参照してください。
API では、注釈の数が 1 回の要求あたり最大 50 に制限されています。 50 を超える注釈を作成するには、"チェック実行の更新" エンドポイントに対して要求を複数回行う必要があります。 たとえば、105 個の注釈を作成するには、API に対して 3 回の個別の要求を行う必要があります。 始めの 2 回のリクエストでそれぞれ 50 個のアノテーションが作成され、3 回目のリクエストで残り 5 つのアノテーションが作成されます。 チェック実行を更新するたびに、アノテーションは既存のチェック実行にあるアノテーションのリストに追加されます。
チェック実行は、アノテーションをオブジェクトの配列として受け取ります。 各アノテーション オブジェクトには、path
、start_line
、end_line
、annotation_level
、message
が含まれている必要があります。 RuboCop により start_column
と end_column
も提供されるので、それらのオプションのパラメーターをアノテーションに含めることができます。 アノテーションの start_column
と end_column
は、同じ行でのみサポートされます。 詳しい情報については、「チェック実行用 REST API エンドポイント」の annotations
オブジェクトを参照してください。
次に、各注釈を作成するために必要な情報を RuboCop から抽出するコードを追加します。
前の手順で追加したコードの下の # ADD ANNOTATIONS CODE HERE #
と書かれている箇所に、次のコードを追加します。
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 /rest/reference/checks#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 # ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #
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 /rest/reference/checks#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
# ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #
このコードでは、アノテーションの合計数を 50 に制限しています。 このコードを、50 のアノテーションごとにチェック実行を更新するよう変更することも可能です。 上のコードには、違反を反復処理するループで使われる変数 max_annotations
が含まれていて、制限が 50 に設定されています。
offense_count
が 0 の場合、CI テストは success
になります。 エラーがある場合、このコードは結果を neutral
に設定します。これは、コード リンターによってエラーが厳格に強制されるのを防ぐためです。 ただし、リンティング エラーがある場合にチェックスイートが失敗になるようにしたい場合は、結果を failure
に変更できます。
エラーが報告されると、上のコードは RuboCop レポートの files
配列を反復処理します。 各ファイルについて、ファイル パスが抽出され、アノテーション レベルが notice
に設定されます。 さらに進んで、RuboCop Cop の種類ごとに特定の警告レベルを設定することもできますが、このチュートリアルでは、簡略化するために、すべてのエラーを notice
のレベルに設定します。
このコードでは、また、offenses
配列の各エラーを反復処理し、違反の場所とエラー メッセージを収集します。 必要な情報を抽出した後、コードでエラーごとにアノテーションを作成して、annotations
配列に格納します。 アノテーションは開始列と終了列が同じ行にある場合にのみサポートされるため、開始行と終了行の値が同じ場合にだけ、start_column
と end_column
が annotation
オブジェクトに追加されます。
このコードはまだチェック実行のアノテーションを作成しません。 それを作成するコードは、次のセクションで追加します。
ステップ 2.5. チェック実行を CI テストの結果で更新する
GitHub から実行される各チェックには、title
、summary
、text
、annotations
、images
を含む output
オブジェクトが含まれます。 output
の必須パラメーターは summary
と title
のみですが、それらだけでは十分に詳細な情報が提供されないため、このチュートリアルでは text
と annotations
も追加します。
summary
については、この例では RuboCop からの概要情報を使用し、出力の書式を設定するために改行 (\n
) を追加します。 text
パラメーターに追加するものはカスタマイズできますが、この例では RuboCop のバージョンを text
パラメーターに設定します。 次のコードでは、summary
と text
を設定します。
前の手順で追加したコードの下の # ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #
と書かれている箇所に、次のコードを追加します。
# 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']}"
# 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']}"
これで、コードでは、チェック実行を更新するために必要なすべての情報が得られます。 「ステップ 1.3. チェック実行の更新」では、チェック実行を success
に設定するコードを追加しました。 そのコードを、RuboCop の結果に基づいて (success
または neutral
に) 設定した conclusion
変数を使うように、更新する必要があります。 前に server.rb
ファイルに追加したコードを次に示します。
# Mark the check run as complete!
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'completed',
conclusion: 'success',
accept: 'application/vnd.github+json'
)
このコードを次のコードに置き換えます。
# Mark the check run as complete! And if there are warnings, share them. @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'completed', conclusion: conclusion, output: { title: 'Octo RuboCop', summary: summary, text: text, annotations: annotations }, actions: [{ label: 'Fix this', description: 'Automatically fix all linter notices.', identifier: 'fix_rubocop_notices' }], accept: 'application/vnd.github+json' )
# Mark the check run as complete! And if there are warnings, share them.
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'completed',
conclusion: conclusion,
output: {
title: 'Octo RuboCop',
summary: summary,
text: text,
annotations: annotations
},
actions: [{
label: 'Fix this',
description: 'Automatically fix all linter notices.',
identifier: 'fix_rubocop_notices'
}],
accept: 'application/vnd.github+json'
)
これで、コードは、CI テストの状態に基づいて conclusion を設定し、RuboCop の結果からの出力を追加するようになったので、CI テストが作成されました。
上記のコードでは、actions
オブジェクトによって、CI サーバーに requested アクションという機能も追加されます (これは、GitHub Actions とは関係がないことに注意してください)。 詳しい情報については、「チェック実行からさらにアクションを要求する」を参照してください。 requested アクションは、追加のアクションを実行するためにチェック実行を要求できるボタンを GitHub の [チェック] タブに追加します。 追加のアクションは、アプリケーションが自由に設定できます。 たとえば、RuboCop には Ruby のコードで見つかったエラーを自動的に修正する機能があるので、CI サーバーはリクエストされたアクションボタンを使用して、自動的なエラー修正をユーザが許可することができます。 このボタンをクリックすると、アプリは requested_action
アクションを含む check_run
イベントを受け取ります。 アプリでは、各要求されたアクションに含まれる identifier
を使って、クリックされたボタンを特定します。
上記のコードには、まだ RuboCop が自動的にエラーを修正する処理がありません。 これは、このチュートリアルで後ほど追加します。
コードのテスト
次の手順は、コードが機能することをテストし、先ほど作成した CI テストを表示する方法を示します。
-
次のコマンドを実行して、ターミナルからサーバーを再起動します。 サーバーが既に実行されている場合は、まずターミナルに「
Ctrl-C
」と入力してサーバーを停止し、次のコマンドを実行してサーバーを再度起動します。Shell ruby server.rb
ruby server.rb
-
myfile.rb
ファイルを追加したリポジトリに、新しい pull request を作成します。 -
先ほど作成した pull request で、 [チェック] タブに移動します。RuboCop で検出された各エラーの注釈が表示されます。 また、requested アクションを追加することによって作成された [これを修正する] ボタンにも注目してください。
ステップ 2.6. RuboCop のエラーを自動的に修正する
ここまでは、CI テストを作成しました。 このセクションでは、もう 1 つの機能を追加します。RuboCop を使用して、見つけたエラーを自動的に修正するために使用するための機能です。 [これを修正する] ボタンは、「ステップ 2.5. チェック実行を CI テストの結果で更新する」で既に追加しました。 次に、[これを修正する] ボタンがクリックされたときにトリガーされる requested_action
チェック実行イベントを処理するコードを追加します。
RuboCop ツールには、検出したエラーを自動的に修正するための --auto-correct
コマンドライン オプションが用意されています。 詳しい情報については、RuboCop のドキュメントの「違反の自動修正」を参照してください。 --auto-correct
機能を使うと、サーバー上のローカル ファイルに更新が適用されます。 RuboCop で修正が行われた後、変更を GitHub にプッシュする必要があります。
リポジトリにプッシュするには、アプリに、リポジトリ内の "コンテンツ" に対する書き込みアクセス許可が必要です。 「ステップ 2.2. RuboCop がテスト リポジトリをクローンできるようにする」で、そのアクセス許可を既に「読み取りと書き込み」に設定しています。
ファイルをコミットするには、Git で、コミットに関連付けるユーザー名とメール アドレスを認識する必要があります。 次に、Git コミットを行うときにアプリで使用する名前とメール アドレスを格納するための環境変数を追加します。
-
このチュートリアルで先ほど作成した
.env
ファイルを開きます。 -
次の環境変数を
.env
ファイルに追加します。APP_NAME
をアプリの名前に置き換え、EMAIL_ADDRESS
を、この例に使用する任意のメールに置き換えます。Shell GITHUB_APP_USER_NAME="APP_NAME" GITHUB_APP_USER_EMAIL="EMAIL_ADDRESS"
GITHUB_APP_USER_NAME="APP_NAME" GITHUB_APP_USER_EMAIL="EMAIL_ADDRESS"
次に、環境変数を読み取り、Git 構成を設定するコードを追加する必要があります。 このコードは、もうすぐ追加することになります。
[これを修正する] ボタンをクリックすると、アプリは requested_action
アクションの種類を含むチェック実行 Webhook を受信します。
「ステップ 1.3. チェック実行を更新する」で、check_run
イベントのアクションを見つけるように server.rb
ファイル内の event_handler
を更新しました。 created
と rerequested
のアクションの種類を処理する case ステートメントは既に存在します。
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
# ADD REQUESTED_ACTION METHOD HERE #
end
end
rerequested
ケースの後の # ADD REQUESTED_ACTION METHOD HERE #
と書かれている箇所に、次のコードを追加します。
when 'requested_action' take_requested_action
when 'requested_action'
take_requested_action
このコードは、アプリに対するすべての requested_action
イベントを処理する新しいメソッドを呼び出します。
helpers do
で始まるコード ブロックの # ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #
と書かれている箇所に、次のヘルパー メソッドを追加します。
# 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
# 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
上記のコードは、「ステップ 2.2. RuboCop がテスト リポジトリをクローンできるようにする」で追加したコードと同じように、リポジトリをクローンします。 if
ステートメントは、要求されたアクションの識別子が RuboCop のボタン識別子 (fix_rubocop_notices
) と一致することを確認します。 それらが一致する場合、コードはリポジトリをクローンし、Git のユーザー名とメール アドレスを設定してから、オプション --auto-correct
を指定して RuboCop を実行します。 --auto-correct
オプションは、ローカル環境の CI サーバー ファイルに変更を自動的に適用します。
ファイルはローカルで変更されますが、GitHub にプッシュする必要があります。 ruby-git
gem を使用して、すべてのファイルをコミットします。 Git には、変更または削除されたすべてのファイルをステージングし、それらをコミットする git commit -a
というコマンドがあります。 ruby-git
を使って同じことを行うため、上のコードでは commit_all
メソッドを使っています。 その後、コードでは、Git clone
コマンドと同じ認証方法を使用して、インストール トークンを使用し、コミットされたファイルを GitHub にプッシュします。 最後に、リポジトリディレクトリを削除して、ワーキングディレクトリが次のイベントに備えるようにします。
これで、作成したコードにより、GitHub App とチェックを使用して構築した継続的インテグレーション サーバーが完成します。 アプリの完全な最終コードを確認するには、「完全なコード例」を参照してください。
コードのテスト
次の手順は、コードが機能することと、RuboCop で検出したエラーを自動的に修正できることをテストする方法を示します。
-
次のコマンドを実行して、ターミナルからサーバーを再起動します。 サーバーが既に実行されている場合は、まずターミナルに「
Ctrl-C
」と入力してサーバーを停止し、次のコマンドを実行してサーバーを再度起動します。Shell ruby server.rb
ruby server.rb
-
myfile.rb
ファイルを追加したリポジトリに、新しい pull request を作成します。 -
作成した新しい pull request で、 [チェック] タブに移動し、[これを修正する] ボタンをクリックして、RuboCop で検出されたエラーを自動的に修正します。
-
[コミット] タブに移動します。Git 構成で設定したユーザー名で新しいコミットが表示されます。 更新を確認するには、ブラウザを更新する必要がある場合があります。
-
[チェック] タブに移動します。Octo RuboCop の新しいチェック スイートが表示されます。 ただし、今度はエラーがありません。RuboCop によってすべて修正されたためです。
完全なコード例
このチュートリアルのすべての手順に従うと、server.rb
内のコードは最終的に次のようになります。 コード全体にわたって、追加のコンテキストを提供するコメントもあります。
require 'sinatra/base' # Use the Sinatra web framework require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API require 'dotenv/load' # Manages environment variables require 'json' # Allows your app to manipulate JSON data require 'openssl' # Verifies the webhook signature require 'jwt' # Authenticates a GitHub App require 'time' # Gets ISO 8601 representation of a Time object require 'logger' # Logs debug statements # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. class GHAapp < Sinatra::Application # Sets the port that's used when starting the web server. set :port, 3000 set :bind, '0.0.0.0' # Expects the private key in PEM format. Converts the newlines. PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a webhook secret. # The secret is used to verify that webhooks are sent by GitHub. WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET'] # The GitHub App's identifier (type integer). APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER'] # Turn on Sinatra's verbose logging during development configure :development do set :logging, Logger::DEBUG end # Executed before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) verify_webhook_signature # 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 authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do # 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 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 when 'requested_action' take_requested_action end end end 200 # success status end helpers do # Create a new check run with status "queued" def create_check_run @installation_client.create_check_run( # [String, Integer, Hash, Octokit Repository object] A GitHub repository. @payload['repository']['full_name'], # [String] The name of your check run. 'Octo RuboCop', # [String] The SHA of the commit to check # The payload structure differs depending on whether a check run or a check suite event occurred. @payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha'], # [Hash] 'Accept' header option, to avoid a warning about the API not being ready for production use. accept: 'application/vnd.github+json' ) end # 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. @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'in_progress', accept: 'application/vnd.github+json' ) 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) # Run RuboCop on all files in the repository @report = `rubocop '#{repository}' --format json` logger.debug @report `rm -rf #{repository}` @output = JSON.parse @report 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 /rest/reference/checks#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 # 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']}" # Mark the check run as complete! And if there are warnings, share them. @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'completed', conclusion: conclusion, output: { title: 'Octo RuboCop', summary: summary, text: text, annotations: annotations }, actions: [{ label: 'Fix this', description: 'Automatically fix all linter notices.', identifier: 'fix_rubocop_notices' }], accept: 'application/vnd.github+json' ) end # 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 # 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 # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) # request.body is an IO or StringIO object # Rewind in case someone already read it request.body.rewind # The raw text of the body is required for webhook signature verification @payload_raw = request.body.read begin @payload = JSON.parse @payload_raw rescue => e fail 'Invalid JSON (#{e}): #{@payload_raw}' end end # Instantiate an Octokit client authenticated as a GitHub App. # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, # so GitHub can be sure that it came from the app and not altered by # a malicious third party. def authenticate_app payload = { # The time that this JWT was issued, _i.e._ now. iat: Time.now.to_i, # JWT expiration time (10 minute maximum) exp: Time.now.to_i + (10 * 60), # Your GitHub App's identifier number iss: APP_IDENTIFIER } # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end # Instantiate an Octokit client, authenticated as an installation of a # GitHub App, to run API operations. def authenticate_installation(payload) @installation_id = payload['installation']['id'] @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] @installation_client = Octokit::Client.new(bearer_token: @installation_token) end # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to # create the hash signature sent in the `X-HUB-Signature` header of each # webhook. This code computes the expected hash signature and compares it to # the signature sent in the `X-HUB-Signature` header. If they don't match, # this request is an attack, and you should reject it. GitHub uses the HMAC # hexdigest to compute the signature. The `X-HUB-Signature` looks something # like this: 'sha1=123456'. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw) halt 401 unless their_digest == our_digest # The X-GITHUB-EVENT header provides the name of the event. # The action value indicates the which action triggered the event. logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? end end # Finally some logic to let us run this server directly from the command line, # or with Rack. Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file # If they are the same—that is, we are running this file directly, call the # Sinatra run method run! if __FILE__ == $0 end
require 'sinatra/base' # Use the Sinatra web framework
require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API
require 'dotenv/load' # Manages environment variables
require 'json' # Allows your app to manipulate JSON data
require 'openssl' # Verifies the webhook signature
require 'jwt' # Authenticates a GitHub App
require 'time' # Gets ISO 8601 representation of a Time object
require 'logger' # Logs debug statements
# This code is a Sinatra app, for two reasons:
# 1. Because the app will require a landing page for installation.
# 2. To easily handle webhook events.
class GHAapp < Sinatra::Application
# Sets the port that's used when starting the web server.
set :port, 3000
set :bind, '0.0.0.0'
# Expects the private key in PEM format. Converts the newlines.
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))
# Your registered app must have a webhook secret.
# The secret is used to verify that webhooks are sent by GitHub.
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
# The GitHub App's identifier (type integer).
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
# Executed before each request to the `/event_handler` route
before '/event_handler' do
get_payload_request(request)
verify_webhook_signature
# 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
authenticate_app
# Authenticate the app installation in order to run API operations
authenticate_installation(@payload)
end
post '/event_handler' do
# 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
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
when 'requested_action'
take_requested_action
end
end
end
200 # success status
end
helpers do
# Create a new check run with status "queued"
def create_check_run
@installation_client.create_check_run(
# [String, Integer, Hash, Octokit Repository object] A GitHub repository.
@payload['repository']['full_name'],
# [String] The name of your check run.
'Octo RuboCop',
# [String] The SHA of the commit to check
# The payload structure differs depending on whether a check run or a check suite event occurred.
@payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha'],
# [Hash] 'Accept' header option, to avoid a warning about the API not being ready for production use.
accept: 'application/vnd.github+json'
)
end
# 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.
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'in_progress',
accept: 'application/vnd.github+json'
)
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)
# Run RuboCop on all files in the repository
@report = `rubocop '#{repository}' --format json`
logger.debug @report
`rm -rf #{repository}`
@output = JSON.parse @report
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 /rest/reference/checks#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
# 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']}"
# Mark the check run as complete! And if there are warnings, share them.
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'completed',
conclusion: conclusion,
output: {
title: 'Octo RuboCop',
summary: summary,
text: text,
annotations: annotations
},
actions: [{
label: 'Fix this',
description: 'Automatically fix all linter notices.',
identifier: 'fix_rubocop_notices'
}],
accept: 'application/vnd.github+json'
)
end
# 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
# 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
# Saves the raw payload and converts the payload to JSON format
def get_payload_request(request)
# request.body is an IO or StringIO object
# Rewind in case someone already read it
request.body.rewind
# The raw text of the body is required for webhook signature verification
@payload_raw = request.body.read
begin
@payload = JSON.parse @payload_raw
rescue => e
fail 'Invalid JSON (#{e}): #{@payload_raw}'
end
end
# Instantiate an Octokit client authenticated as a GitHub App.
# GitHub App authentication requires that you construct a
# JWT (https://jwt.io/introduction/) signed with the app's private key,
# so GitHub can be sure that it came from the app and not altered by
# a malicious third party.
def authenticate_app
payload = {
# The time that this JWT was issued, _i.e._ now.
iat: Time.now.to_i,
# JWT expiration time (10 minute maximum)
exp: Time.now.to_i + (10 * 60),
# Your GitHub App's identifier number
iss: APP_IDENTIFIER
}
# Cryptographically sign the JWT.
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
# Create the Octokit client, using the JWT as the auth token.
@app_client ||= Octokit::Client.new(bearer_token: jwt)
end
# Instantiate an Octokit client, authenticated as an installation of a
# GitHub App, to run API operations.
def authenticate_installation(payload)
@installation_id = payload['installation']['id']
@installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token]
@installation_client = Octokit::Client.new(bearer_token: @installation_token)
end
# Check X-Hub-Signature to confirm that this webhook was generated by
# GitHub, and not a malicious third party.
#
# GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to
# create the hash signature sent in the `X-HUB-Signature` header of each
# webhook. This code computes the expected hash signature and compares it to
# the signature sent in the `X-HUB-Signature` header. If they don't match,
# this request is an attack, and you should reject it. GitHub uses the HMAC
# hexdigest to compute the signature. The `X-HUB-Signature` looks something
# like this: 'sha1=123456'.
def verify_webhook_signature
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
method, their_digest = their_signature_header.split('=')
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
halt 401 unless their_digest == our_digest
# The X-GITHUB-EVENT header provides the name of the event.
# The action value indicates the which action triggered the event.
logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil?
end
end
# Finally some logic to let us run this server directly from the command line,
# or with Rack. Don't worry too much about this code. But, for the curious:
# $0 is the executed file
# __FILE__ is the current file
# If they are the same—that is, we are running this file directly, call the
# Sinatra run method
run! if __FILE__ == $0
end
次の手順
以上で、API イベントを受信し、チェック実行を作成し、RuboCop を使用して Ruby のエラーを検出し、pull request で注釈を作成し、リンター エラーを自動的に修正するアプリが完成しました。 次に、アプリのコードを展開し、アプリをデプロイして、アプリを公開することができます。
質問がある場合は、API カテゴリと Webhook カテゴリで GitHub Community ディスカッションを開始します。
アプリのコードを変更する
このチュートリアルでは、リポジトリの pull request に常に表示される [これを修正する] ボタンを作成する方法について説明しました。 RuboCop でエラーが検出されたときにのみ [これを修正する] ボタンが表示するようにコードを更新ししてみましょう。
RuboCop でファイルを head ブランチに直接コミットしたくない場合は、head ブランチをベースとする新しいブランチで pull request を作成するようにコードを更新します。
アプリのデプロイ
このチュートリアルでは、アプリをローカルで開発する方法を示しました。 アプリをデプロイする準備ができたら、アプリを提供し、アプリの資格情報をセキュリティで保護するために変更を加える必要があります。 実行する手順は使うサーバーによって異なりますが、以下のセクションでは一般的なガイダンスを提供します。
サーバーでアプリをホストする
このチュートリアルでは、コンピュータまたは codespace をサーバーとして使用しました。 アプリを運用環境で使用する準備ができたら、アプリを専用サーバーにデプロイする必要があります。 たとえば、Azure App Service を使用できます。
Webhook URL を更新する
GitHub から Webhook トラフィックを受信するようにサーバーを設定したら、アプリの設定で Webhook URL を更新します。 運用環境では Webhook を転送するために Smee.io を使用しないでください。
:port
設定を更新する
アプリをデプロイするときに、サーバーがリッスンするポートを変更する必要があります。 このコードでは既に、:bind
を 0.0.0.0
に設定することで、使用可能なすべてのネットワーク インターフェイスをリッスンするようにサーバーに指示しています。
たとえば、サーバー上の .env
ファイル内の PORT
環境変数を設定して、サーバーがリッスンする必要があるポートを指示することができます。 この後、サーバーがデプロイ ポートをリッスンするように、コードで :port
を設定する箇所を更新できます。
set :port, ENV['PORT']
set :port, ENV['PORT']
アプリの資格情報をセキュリティで保護する
アプリの秘密キーや Webhook シークレットは絶対に公開しないでください。 このチュートリアルでは、gitignore の .env
ファイルにアプリの資格情報を格納しました。 アプリをデプロイする際に、資格情報を保存する安全な方法を選び、それに応じて値を取得するようにコードを更新する必要があります。 たとえば、Azure Key Vault などのシークレット管理サービスを使って資格情報を格納できます。 アプリを実行するときに、アプリで資格情報を取得し、アプリがデプロイされているサーバー上の環境変数に格納できます。
詳しくは、「GitHub App を作成するためのベスト プラクティス」を参照してください。
アプリを共有する
アプリを他のユーザーや組織と共有したい場合は、アプリを公開します。 詳しくは、「GitHub Appをパブリックまたはプライベートにする」を参照してください。
ベスト プラクティスに従う
GitHub App に関するベスト プラクティスに従うようにする必要があります。 詳しくは、「GitHub App を作成するためのベスト プラクティス」を参照してください。