简介
本教程演示如何构建一个持续集成 (CI) 服务器,该服务器在将新代码推送到仓库时运行测试。本教程展示了如何构建和配置一个 GitHub 应用,以充当服务器,使用 GitHub 的 REST API 接收和响应 check_run
和 check_suite
webhook 事件。
在本教程中,您将在开发应用程序时使用您的计算机或代码空间作为服务器。应用程序准备好投入生产使用后,您应该将应用程序部署到专用服务器。
本教程使用 Ruby,但您可以在服务器上运行的任何编程语言中使用。
本教程分为两部分
- 在第一部分中,您将学习如何使用 GitHub 的 REST API 设置 CI 服务器的框架,在仓库收到新推送的提交时创建新的检查运行,以及在用户在 GitHub 上请求该操作时重新运行检查运行。
- 在第二部分中,您将通过将 linter 测试添加到 CI 服务器来为 CI 测试添加功能。您还将创建在拉取请求的“检查”和“已更改文件”选项卡中显示的注释,并通过在拉取请求的“检查”选项卡中公开“修复此问题”按钮来自动修复 linter 建议。
关于持续集成 (CI)
CI 是一种软件实践,要求频繁地将代码提交到共享仓库。更频繁地提交代码可以更快地发现错误,并减少开发人员在查找错误源时需要调试的代码量。频繁的代码更新还可以更轻松地合并来自软件开发团队不同成员的更改。这对开发人员来说非常棒,他们可以花更多时间编写代码,而花更少时间调试错误或解决合并冲突。
CI 服务器托管运行 CI 测试的代码,例如代码 linter(检查样式格式)、安全检查、代码覆盖率以及对仓库中新代码提交的其他检查。CI 服务器甚至可以构建代码并将其部署到暂存或生产服务器。有关使用 GitHub 应用可以创建的 CI 测试类型的示例,请参阅 GitHub Marketplace 中提供的 持续集成应用。
关于检查
GitHub 的 REST API 允许您设置 CI 测试(检查),这些测试会自动针对存储库中的每个代码提交运行。API 会在 GitHub 上拉取请求的 **检查** 选项卡中报告有关每个检查的详细信息。您可以在存储库中使用检查来确定代码提交何时引入错误。
检查包括检查运行、检查套件和提交状态。
- 检查运行 是在提交上运行的单个 CI 测试。
- 检查套件 是检查运行的组。
- 提交状态 标记提交的状态,例如
error
、failure
、pending
或success
,并且在 GitHub 上的拉取请求中可见。检查套件和检查运行都包含提交状态。
GitHub 使用默认流程自动为存储库中的新代码提交创建 check_suite
事件,尽管您可以更改默认设置。有关更多信息,请参阅“检查套件的 REST API 端点”。以下是默认流程的工作原理
- 当有人将代码推送到存储库时,GitHub 会自动将
check_suite
事件(操作为requested
)发送到安装在存储库上的所有具有checks:write
权限的 GitHub 应用程序。此事件让应用程序知道代码已推送到存储库,并且 GitHub 已自动创建新的检查套件。 - 当您的应用程序收到此事件时,它可以将检查运行添加到该套件。
- 您的检查运行可以包括显示在代码特定行上的注释。注释在 **检查** 选项卡中可见。当您为拉取请求的一部分的文件创建注释时,注释也会显示在 **已更改的文件** 选项卡中。有关更多信息,请参阅“检查运行的 REST API 端点”中的
annotations
对象。
有关检查的更多信息,请参阅“检查的 REST API 端点”和“使用 REST API 与检查交互”。
先决条件
本教程假设您对 Ruby 编程语言 有基本了解。
在开始之前,您可能需要熟悉以下概念
检查也可以与 GraphQL API 一起使用,但本教程重点介绍 REST API。有关 GraphQL 对象的更多信息,请参阅 GraphQL 文档中的 检查套件 和 检查运行。
设置
以下部分将引导您完成以下组件的设置
- 一个用于存储应用程序代码的仓库。
- 一种在本地接收 Webhook 的方法。
- 一个订阅了“检查套件”和“检查运行”Webhook 事件的 GitHub 应用程序,它对检查具有写入权限,并使用您可以在本地接收的 Webhook URL。
创建一个仓库来存储您的 GitHub 应用程序代码
-
创建一个仓库来存储您的应用程序代码。有关更多信息,请参阅“创建新仓库”。
-
从上一步克隆您的仓库。有关更多信息,请参阅“克隆仓库”。您可以使用本地克隆或 GitHub Codespaces。
-
在终端中,导航到存储克隆的目录。
-
创建一个名为
server.rb
的 Ruby 文件。此文件将包含您的应用程序的所有代码。您将在稍后向此文件添加内容。 -
如果目录中尚未包含
.gitignore
文件,请添加一个.gitignore
文件。您将在稍后向此文件添加内容。有关.gitignore
文件的更多信息,请参阅“忽略文件”。 -
创建一个名为
Gemfile
的文件。此文件将描述您的 Ruby 代码所需的 gem 依赖项。将以下内容添加到您的Gemfile
中Ruby source 'https://rubygems.org.cn' gem 'sinatra', '~> 2.0' gem 'jwt', '~> 2.1' gem 'octokit', '~> 4.0' gem 'puma' gem 'rubocop' gem 'dotenv' gem 'git'
source 'https://rubygems.org.cn' 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 的 Webhook 事件转发到您的计算机或 Codespace。本教程使用 Smee.io 来提供 Webhook 代理 URL 并转发事件。
-
在终端中,运行以下命令来安装 Smee 客户端
Shell npm install --global smee-client
npm install --global smee-client
-
在浏览器中,导航到 https://smee.io/.
-
点击开始新频道。
-
复制“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 将 Smee 通道接收到的所有 webhook 事件转发到运行在您计算机上的 Smee 客户端。选项 --path /event_handler
将事件转发到 /event_handler
路由。选项 --port 3000
指定端口 3000,这是您在稍后教程中添加更多代码时将告诉您的服务器监听的端口。使用 Smee,您的机器无需公开到公共互联网即可接收来自 GitHub 的 webhook。您也可以在浏览器中打开该 Smee URL 以检查传入的 webhook 有效负载。
我们建议您在完成本指南中的其余步骤时保持此终端窗口打开并保持 Smee 连接。虽然您可以断开 Smee 客户端的连接并重新连接而不会丢失您的唯一域,但您可能会发现将其保持连接状态并在不同的终端窗口中执行其他命令行任务更容易。
注册 GitHub 应用
在本教程中,您必须注册一个 GitHub 应用,该应用
- 具有活动的 webhook
- 使用您可以本地接收的 webhook URL
- 具有“检查”存储库权限
- 订阅“检查套件”和“检查运行”webhook 事件
以下步骤将指导您完成使用这些设置配置 GitHub 应用的过程。有关 GitHub 应用设置的更多信息,请参阅“注册 GitHub 应用”。
- 在 GitHub 上任何页面的右上角,单击您的个人资料照片。
- 导航到您的帐户设置。
- 对于个人帐户拥有的应用,单击**设置**。
- 对于组织拥有的应用
- 单击**您的组织**。
- 在组织的右侧,单击**设置**。
- 在左侧边栏中,单击** 开发者设置**。
- 在左侧边栏中,单击**GitHub 应用**。
- 单击**新建 GitHub 应用**。
- 在“GitHub 应用名称”下,输入您的应用名称。例如,
USERNAME-ci-test-app
,其中USERNAME
是您的 GitHub 用户名。 - 在“主页 URL”下,输入您的应用的 URL。例如,您可以使用您创建的用于存储应用代码的仓库的 URL。
- 在本教程中,跳过“识别和授权用户”和“安装后”部分。
- 确保在“Webhooks”下选择了活动。
- 在“Webhook URL”下,输入您之前获得的 webhook 代理 URL。有关更多信息,请参阅“获取 webhook 代理 URL”。
- 在“Webhook 密钥”下,输入一个随机字符串。此密钥用于验证 webhook 是否由 GitHub 发送。保存此字符串;您将在后面使用它。
- 在“仓库权限”下,在“检查”旁边,选择读写。
- 在“订阅事件”下,选择检查套件和检查运行。
- 在“此 GitHub 应用可以在哪里安装?”下,选择仅在此帐户上。如果您想发布您的应用,您可以稍后更改此设置。
- 单击创建 GitHub 应用。
存储您的应用的识别信息和凭据
本教程将向您展示如何将您的应用的凭据和识别信息存储为 .env
文件中的环境变量。部署应用时,您应该更改存储凭据的方式。有关更多信息,请参阅“部署您的应用”。
在执行这些步骤之前,请确保您在安全的机器上,因为您将在本地存储您的凭据。
-
在您的终端中,导航到您的克隆存储的目录。
-
在此目录的顶层创建一个名为
.env
的文件。 -
将
.env
添加到您的.gitignore
文件中。这将防止您意外提交您的应用的凭据。 -
将以下内容添加到您的
.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 应用**。
-
在您的应用名称旁边,单击编辑。
-
-
在您的应用的设置页面上,在“应用 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 应用程序的代码
本节将向您展示如何为您的 GitHub 应用程序添加一些基本的模板代码,并解释代码的作用。在本教程的后面,您将学习如何修改和添加此代码,以构建您的应用程序的功能。
将以下模板代码添加到您的 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.node.org.cn/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.node.org.cn/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 应用程序的所有代码。以下部分详细解释了该类中的代码的作用。
设置端口
在 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'
读取环境变量
接下来,此类将读取您在“存储应用程序的标识信息和凭据”中设置的三个环境变量,并将它们存储在变量中以供以后使用。
# 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
块调用四个辅助方法: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
块中调用了四个辅助方法。helpers do
代码块定义了这些辅助方法中的每一个。
处理 webhook 负载
第一个辅助方法 get_payload_request
捕获 webhook 负载并将其转换为 JSON 格式,这使得访问负载的数据变得更加容易。
验证 webhook 签名
第二个辅助方法 verify_webhook_signature
执行 webhook 签名的验证,以确保 GitHub 生成了该事件。要了解有关 verify_webhook_signature
辅助方法中代码的更多信息,请参阅“验证 webhook 传递”。如果 webhook 是安全的,此方法将把所有传入的负载记录到您的终端。记录器代码有助于验证您的 Web 服务器是否正常工作。
以 GitHub 应用身份进行身份验证
第三个辅助方法 authenticate_app
允许您的 GitHub 应用进行身份验证,以便它可以请求安装令牌。
要进行 API 调用,您将使用 Octokit 库。使用此库执行任何有趣的操作都需要您的 GitHub 应用进行身份验证。有关 Octokit 库的更多信息,请参阅 Octokit 文档。
GitHub 应用有三种身份验证方法
- 使用 JSON Web 令牌 (JWT) 以 GitHub 应用身份进行身份验证。
- 使用安装访问令牌以特定 GitHub 应用安装的身份进行身份验证。
- 代表用户进行身份验证。本教程不会使用这种身份验证方法。
您将在下一节“以安装身份进行身份验证”中了解有关以安装身份进行身份验证的信息。
以 GitHub 应用身份进行身份验证可以让您做几件事
- 您可以检索有关您的 GitHub 应用的高级管理信息。
- 您可以请求应用安装的访问令牌。
例如,您将以 GitHub 应用身份进行身份验证以检索已安装您的应用的帐户(组织和个人)列表。但是,这种身份验证方法不允许您对 API 执行太多操作。要访问存储库的数据并代表安装执行操作,您需要以安装身份进行身份验证。为此,您需要先以 GitHub 应用身份进行身份验证以请求安装访问令牌。有关更多信息,请参阅“关于使用 GitHub 应用进行身份验证”。
在您可以使用 Octokit.rb 库进行 API 调用之前,您需要使用 authenticate_app
辅助方法初始化以 GitHub 应用身份进行身份验证的 Octokit 客户端。
# Instantiate an Octokit client authenticated as a GitHub App.
# GitHub App authentication requires that you construct a
# JWT (https://jwt.node.org.cn/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)”。
以安装身份进行身份验证
第四个也是最后一个辅助方法 authenticate_installation
初始化以安装身份进行身份验证的 Octokit 客户端,您可以使用它对 API 进行身份验证调用。
安装 指的是任何安装了该应用程序的用户或组织帐户。即使某人授予该应用程序对该帐户上的多个存储库的访问权限,它也只算作一次安装,因为它位于同一个帐户中。
# 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”。
此方法接受两个参数
- 安装(整数):GitHub App 安装的 ID
- 选项(哈希,默认为
{}
):一组可自定义的选项
每当 GitHub App 接收 Webhook 时,它都会包含一个带有 id
的 installation
对象。使用以 GitHub App 身份验证的客户端,您可以将此 ID 传递给 create_app_installation_access_token
方法,为每个安装生成访问令牌。由于您没有向该方法传递任何选项,因此选项默认为空哈希。create_app_installation_access_token
的响应包含两个字段:token
和 expired_at
。模板代码选择响应中的令牌并初始化安装客户端。
有了此方法,每次您的应用程序收到新的 Webhook 有效负载时,它都会为触发该事件的安装创建客户端。此身份验证过程使您的 GitHub App 能够为任何帐户上的所有安装工作。
启动服务器
您的应用程序目前还没有执行任何操作,但此时,您可以在服务器上运行它。
-
在您的终端中,确保 Smee 仍在运行。有关更多信息,请参阅“获取 Webhook 代理 URL”。
-
在您的终端中打开一个新标签,并
cd
到您在教程中之前克隆的存储库所在的目录。有关更多信息,请参阅“创建存储库以存储您的 GitHub App 的代码”。此存储库中的 Ruby 代码将启动一个 Sinatra Web 服务器。 -
通过运行以下两个命令(一个接一个)来安装依赖项
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` 文件。
-
要测试服务器,请在浏览器中导航到 `https://127.0.0.1:3000`。
如果您看到一个错误页面,上面写着“Sinatra doesn't know this ditty”,则应用程序按预期工作。即使它是一个错误页面,它也是一个 Sinatra 错误页面,这意味着您的应用程序已按预期连接到服务器。您看到此消息是因为您没有给应用程序提供其他任何内容来显示。
测试服务器是否正在监听您的应用程序
您可以通过触发一个事件来测试服务器是否正在监听您的应用程序,以便它接收该事件。您将通过在测试存储库中安装应用程序来执行此操作,这将向您的应用程序发送 installation
事件。如果应用程序接收了它,您应该在运行 `server.rb` 的终端选项卡中看到输出。
-
创建一个新的存储库来用于测试您的教程代码。有关更多信息,请参阅“创建新的存储库”。
-
在您刚刚创建的存储库上安装 GitHub 应用程序。有关更多信息,请参阅“安装您自己的 GitHub 应用程序”。在安装过程中,选择**仅选择存储库**,然后选择您在上一步中创建的存储库。
-
单击**安装**后,查看运行 `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 应用程序的代码”中添加到 `server.rb` 的应用程序模板代码中编写的。
第 1 部分。创建 Checks API 接口
在本部分中,您将添加接收 `check_suite` webhook 事件以及创建和更新检查运行所需的代码。您还将学习如何在 GitHub 上重新请求检查时创建检查运行。在本节结束时,您将能够在 GitHub 拉取请求中查看您创建的检查运行。
您的检查运行不会对本节中的代码执行任何检查。您将在“第 2 部分:创建 CI 测试”中添加该功能。
您应该已经配置了一个 Smee 频道,该频道将 Webhook 负载转发到您的本地服务器。您的服务器应该正在运行并连接到您注册并安装在测试存储库上的 GitHub 应用程序。
以下是在第 1 部分中将完成的步骤
步骤 1.1. 添加事件处理
由于您的应用程序订阅了 **检查套件** 和 **检查运行** 事件,它将接收 check_suite
和 check_run
Webhook。GitHub 以 POST
请求的形式发送 Webhook 负载。由于您将 Smee Webhook 负载转发到 https://127.0.0.1:3000/event_handler
,因此您的服务器将在 post '/event_handler'
路由处接收 POST
请求负载。
打开您在“为您的 GitHub 应用程序添加代码”中创建的 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
。在此代码中,我们将检查运行命名为“Octo RuboCop”,因为我们将在本教程的后面使用 RuboCop 来实现 CI 测试。但是您可以为检查运行选择任何您喜欢的名称。有关 RuboCop 的更多信息,请参阅 RuboCop 文档。
您现在只提供必需的参数来使基本功能正常工作,但您将在收集有关检查运行的更多信息后更新检查运行。默认情况下,GitHub 将 status
设置为 queued
。
GitHub 为特定提交 SHA 创建检查运行,这就是 head_sha
是必需参数的原因。您可以在 Webhook 负载中找到提交 SHA。虽然您现在只为 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
-
在您在 "测试服务器是否正在监听您的应用程序" 中创建的测试存储库中创建一个拉取请求。这是您授予应用程序访问权限的存储库。
-
在您刚刚创建的拉取请求中,导航到 **检查** 选项卡。您应该看到一个名为“Octo RuboCop”的检查运行,或者您之前为检查运行选择的任何名称。
如果您在 **检查** 选项卡中看到其他应用程序,则表示您在存储库中安装了其他应用程序,这些应用程序对检查具有 **读写** 访问权限,并且订阅了 **检查套件** 和 **检查运行** 事件。这也可能意味着您在存储库中具有由 pull_request
或 pull_request_target
事件触发的 GitHub Actions 工作流程。
到目前为止,你已经告诉 GitHub 创建一个检查运行。拉取请求中的检查运行状态设置为排队,并显示黄色图标。在下一步中,你将等待 GitHub 创建检查运行并更新其状态。
步骤 1.3. 更新检查运行
当你的 create_check_run
方法运行时,它会要求 GitHub 创建一个新的检查运行。当 GitHub 完成创建检查运行后,你将收到带有 created
操作的 check_run
webhook 事件。该事件是你的信号,表明可以开始运行检查。
你将更新你的事件处理程序以查找 created
操作。在更新事件处理程序时,你可以添加一个针对 rerequested
操作的条件。当有人通过点击“重新运行”按钮在 GitHub 上重新运行单个测试时,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
检查运行与 requested
或 rerequested
检查套件略有不同,GitHub 仅将这些套件发送到被要求运行检查的应用程序。上面的代码查找检查运行的应用程序 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 部分中,您将添加在 `***** RUN A CI TEST *****` 下启动真实 CI 测试的代码。现在,您将保留该部分作为占位符,因此后面的代码将模拟 CI 过程成功且所有测试都通过。最后,代码再次将检查运行的状态更新为 `completed`。
当您使用 REST API 提供 `completed` 的检查运行状态时,需要 `conclusion` 和 `completed_at` 参数。`conclusion` 总结了检查运行的结果,可以是 `success`、`failure`、`neutral`、`cancelled`、`timed_out`、`skipped` 或 `action_required`。您将结论设置为 `success`,`completed_at` 时间设置为当前时间,状态设置为 `completed`。
您也可以提供有关检查正在执行的操作的更多详细信息,但您将在下一节中了解这一点。
测试代码
以下步骤将向您展示如何测试代码是否有效,以及您创建的新的“重新运行所有”按钮是否有效。
-
运行以下命令从您的终端重新启动服务器。如果服务器已在运行,请先在您的终端中输入
Ctrl-C
停止服务器,然后运行以下命令重新启动服务器。Shell ruby server.rb
ruby server.rb
-
在您在 "测试服务器是否正在监听您的应用程序" 中创建的测试存储库中创建一个拉取请求。这是您授予应用程序访问权限的存储库。
-
在您刚刚创建的拉取请求中,导航到 **Checks** 选项卡。您应该看到一个“重新运行所有”按钮。
-
单击右上角的“重新运行所有”按钮。测试应该再次运行,并以 `success` 结束。
第 2 部分。创建 CI 测试
既然您已经创建了接收 API 事件并创建检查运行的界面,您就可以创建一个实现 CI 测试的检查运行。
RuboCop 是一个 Ruby 代码 linter 和格式化程序。它检查 Ruby 代码以确保它符合 Ruby 样式指南。有关更多信息,请参阅 RuboCop 文档。
RuboCop 有三个主要功能
- Linting 以检查代码风格
- 代码格式化
- 使用 `ruby -w` 替换本机 Ruby linting 功能
您的应用程序将在 CI 服务器上运行 RuboCop,并创建检查运行(在本例中为 CI 测试),这些检查运行会将 RuboCop 报告给 GitHub 的结果报告给 GitHub。
REST API 允许您报告有关每个检查运行的丰富详细信息,包括状态、图像、摘要、注释和请求的操作。
注释是关于存储库中特定代码行的信息。注释允许您精确定位并可视化您想要显示附加信息的代码的精确部分。例如,您可以在特定代码行上显示信息作为注释、错误或警告。本教程使用注释来可视化 RuboCop 错误。
为了利用请求的操作,应用程序开发人员可以在拉取请求的 **检查** 选项卡中创建按钮。当有人点击这些按钮之一时,点击会将 requested_action
check_run
事件发送到 GitHub 应用程序。应用程序执行的操作完全由应用程序开发人员配置。本教程将引导您添加一个按钮,允许用户请求 RuboCop 修复它找到的错误。RuboCop 支持使用命令行选项自动修复错误,您将配置 requested_action
来利用此选项。
以下是您将在本节中完成的步骤
步骤 2.1. 添加一个 Ruby 文件
您可以传递特定文件或整个目录供 RuboCop 检查。在本教程中,您将在整个目录上运行 RuboCop。RuboCop 仅检查 Ruby 代码。要测试您的 GitHub 应用程序,您需要在您的存储库中添加一个包含 RuboCop 要查找的错误的 Ruby 文件。将以下 Ruby 文件添加到您的存储库后,您将更新您的 CI 检查以在代码上运行 RuboCop。
-
导航到您在 "测试服务器是否正在监听您的应用程序" 中创建的测试存储库。这是您授予应用程序访问权限的存储库。
-
创建一个名为
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,您的 GitHub 应用需要在 CI 服务器上克隆仓库的本地副本,以便 RuboCop 解析文件。为此,您的代码需要能够运行 Git 操作,并且您的 GitHub 应用需要具有克隆仓库的正确权限。
允许 Git 操作
要在 Ruby 应用中运行 Git 操作,可以使用 ruby-git gem。您在“设置”中创建的 Gemfile
已经包含了 ruby-git gem,并且您在“启动服务器”中运行 bundle install
时已经安装了它。
现在,在您的 server.rb
文件顶部,在其他 require
项目下方,添加以下代码
require 'git'
require 'git'
更新您的应用权限
接下来,您需要更新您的 GitHub 应用的权限。您的应用需要对“内容”的读取权限才能克隆仓库。在本教程的后面,它还需要写入权限才能将内容推送到 GitHub。要更新您的应用的权限
- 从 应用设置页面 选择您的应用,然后单击侧边栏中的 **权限和事件**。
- 在“仓库权限”下,在“内容”旁边,选择 **读写**。
- 单击页面底部的 **保存更改**。
- 如果您已在您的帐户上安装了该应用,请查看您的电子邮件并按照链接接受新的权限。每次更改应用的权限或 Webhook 时,安装了该应用的用户(包括您自己)都需要接受新的权限,然后更改才会生效。您也可以通过导航到您的 安装页面 接受新的权限。您将在应用名称下看到一个链接,告知您该应用正在请求不同的权限。单击 **查看请求**,然后单击 **接受新权限**。
添加克隆仓库的代码
要克隆仓库,代码将使用您的 GitHub 应用的权限和 Octokit SDK 为您的应用创建安装令牌(x-access-token:TOKEN
),并在以下克隆命令中使用它
git clone https://x-access-token:[email protected]/OWNER/REPO.git
上面的命令通过 HTTP 克隆仓库。它需要完整的仓库名称,包括仓库所有者(用户或组织)和仓库名称。例如,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 ref(@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 #
上面的代码从check_run
webhook 负载中获取完整的存储库名称和提交的头部 SHA。
步骤 2.3. 运行 RuboCop
到目前为止,您的代码克隆了存储库并使用您的 CI 服务器创建了检查运行。现在您将深入了解RuboCop linter和检查注释。
首先,您将添加代码来运行 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
以机器可解析的格式保存 linting 结果的副本。有关更多信息以及 JSON 格式的示例,请参阅 RuboCop 文档中的“JSON 格式化程序”。此代码还解析 JSON,以便您可以使用@output
变量轻松访问 GitHub 应用程序中的键和值。
运行 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
文件的仓库中,创建一个新的拉取请求。 -
在您的服务器正在运行的终端选项卡中,您应该会看到包含代码风格检查错误的调试输出。代码风格检查错误将以无格式的方式打印。您可以将调试输出复制粘贴到像 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
数组中的每个文件报告错误,因此如果有错误,您需要从文件对象中提取一些数据。
用于管理检查运行的 REST API 端点允许您为特定代码行创建注释。当您创建或更新检查运行时,您可以添加注释。在本教程中,您将使用 PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
端点,使用注释更新检查运行。有关该端点的更多信息,请参阅 "检查运行的 REST API 端点."
API 将每个请求的注释数量限制为最多 50 个。要创建超过 50 个注释,您需要向“更新检查运行”端点发出多个请求。例如,要创建 105 个注释,您需要向 API 发出三个单独的请求。前两个请求将分别包含 50 个注释,第三个请求将包含剩余的五个注释。每次更新检查运行时,注释都会附加到已存在于检查运行中的注释列表中。
检查运行将注释作为对象数组。每个注释对象必须包含 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
为零时,CI 测试为 success
。如果存在错误,此代码将结论设置为 neutral
以防止严格执行代码 linter 的错误。但是,如果您希望在出现 linting 错误时确保检查套件失败,则可以将结论更改为 failure
。
当报告错误时,上面的代码将遍历 RuboCop 报告中的 files
数组。对于每个文件,它将提取文件路径并将注释级别设置为 notice
。您可以更进一步,为每种类型的 RuboCop Cop 设置特定的警告级别,但为了在本教程中保持简单,所有错误都设置为 notice
级别。
此代码还遍历 offenses
数组中的每个错误,并收集违规和错误消息的位置。提取所需信息后,代码将为每个错误创建一个注释并将其存储在 annotations
数组中。由于注释仅支持同一行上的起始列和结束列,因此仅当起始行值和结束行值相同时,才会将 start_column
和 end_column
添加到 annotation
对象中。
此代码尚未为检查运行创建注释。您将在下一节中添加该代码。
步骤 2.5。使用 CI 测试结果更新检查运行
GitHub 中的每个检查运行都包含一个 output
对象,其中包含 title
、summary
、text
、annotations
和 images
。summary
和 title
是 output
的唯一必需参数,但它们本身并没有提供太多细节,因此本教程还添加了 text
和 annotations
。
对于 summary
,此示例使用 RuboCop 的摘要信息并添加换行符 (\n
) 以格式化输出。您可以自定义添加到 text
参数的内容,但此示例将 text
参数设置为 RuboCop 版本。以下代码设置 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 结果设置的conclusion
变量(设置为success
或neutral
)。以下是您之前添加到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 测试的状态设置结论,并添加 RuboCop 结果的输出,您已经创建了一个 CI 测试。
上面的代码还通过actions
对象在您的 CI 服务器中添加了一个名为请求操作的功能。(注意,这与GitHub Actions无关。)有关更多信息,请参阅“从检查运行请求进一步操作”。请求操作在 GitHub 的“检查”选项卡中添加一个按钮,允许用户请求检查运行执行其他操作。其他操作完全由您的应用程序配置。例如,由于 RuboCop 具有自动修复在 Ruby 代码中找到的错误的功能,因此您的 CI 服务器可以使用请求操作按钮来允许用户请求自动错误修复。当用户单击按钮时,应用程序会收到带有requested_action
操作的check_run
事件。每个请求的操作都有一个identifier
,应用程序使用它来确定单击了哪个按钮。
上面的代码还没有让 RuboCop 自动修复错误。您将在本教程的后面添加它。
测试代码
以下步骤将向您展示如何测试代码是否有效以及查看您刚刚创建的 CI 测试。
-
运行以下命令从您的终端重新启动服务器。如果服务器已在运行,请先在您的终端中输入
Ctrl-C
停止服务器,然后运行以下命令重新启动服务器。Shell ruby server.rb
ruby server.rb
-
在您添加了
myfile.rb
文件的仓库中,创建一个新的拉取请求。 -
在您刚刚创建的拉取请求中,导航到“检查”选项卡。您应该看到 RuboCop 找到的每个错误的注释。还要注意您通过添加请求操作创建的“修复此问题”按钮。
步骤 2.6. 自动修复 RuboCop 错误
到目前为止,您已经创建了一个 CI 测试。在本节中,您将添加另一个功能,该功能使用 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. 更新检查运行”中,您更新了 server.rb
文件中的 event_handler
以查找 check_run
事件中的操作。您已经有一个 case 语句来处理 created
和 rerequested
操作类型
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
case 之后,在显示 # 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
方法。然后,代码使用安装令牌将已提交的文件推送到 GitHub,使用与 Git clone
命令相同的身份验证方法。最后,它删除了仓库目录,以确保工作目录已准备好处理下一个事件。
您编写的代码现在完成了您使用 GitHub 应用程序和检查构建的持续集成服务器。要查看应用程序的完整最终代码,请参阅“完整代码示例”。
测试代码
以下步骤将向您展示如何测试代码是否正常工作,以及 RuboCop 如何自动修复它找到的错误。
-
运行以下命令从您的终端重新启动服务器。如果服务器已在运行,请先在您的终端中输入
Ctrl-C
停止服务器,然后运行以下命令重新启动服务器。Shell ruby server.rb
ruby server.rb
-
在您添加了
myfile.rb
文件的仓库中,创建一个新的拉取请求。 -
在您创建的新拉取请求中,导航到 **检查** 选项卡,然后单击“修复此问题”按钮以自动修复 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.node.org.cn/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.node.org.cn/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 错误、在拉取请求中创建注释并自动修复 linter 错误的应用程序。接下来,您可能希望扩展应用程序的代码、部署应用程序并公开您的应用程序。
如果您有任何问题,请在 API 和 Webhook 类别中启动一个 GitHub 社区讨论。
修改应用程序代码
本教程演示了如何创建一个始终在存储库的拉取请求中显示的“修复此问题”按钮。尝试更新代码以仅在 RuboCop 找到错误时显示“修复此问题”按钮。
如果您希望 RuboCop 不直接将文件提交到 head 分支,请更新代码以改为创建一个基于 head 分支的新分支的拉取请求。
部署您的应用程序
本教程演示了如何在本地开发您的应用程序。当您准备好部署应用程序时,您需要进行更改以服务您的应用程序并确保应用程序的凭据安全。您采取的步骤取决于您使用的服务器,但以下部分提供了一般性指导。
将您的应用托管在服务器上
本教程使用您的计算机或代码空间作为服务器。当您的应用准备好投入生产使用时,您应该将您的应用部署到专用服务器。例如,您可以使用 Azure 应用服务。
更新 Webhook URL
一旦您拥有一个设置为接收来自 GitHub 的 Webhook 流量的服务器,请在您的应用设置中更新 Webhook URL。您不应该在生产环境中使用 Smee.io 来转发您的 Webhook。
更新 :port
设置
当您部署您的应用时,您需要更改服务器监听的端口。代码已经告诉您的服务器通过将 :bind
设置为 0.0.0.0
来监听所有可用的网络接口。
例如,您可以在服务器上的 .env
文件中设置 PORT
变量来指示服务器应该监听的端口。然后,您可以更新代码设置 :port
的位置,以便您的服务器监听您的部署端口。
set :port, ENV['PORT']
set :port, ENV['PORT']
保护您的应用凭据
您永远不应该公开您的应用的私钥或 Webhook 密钥。本教程将您的应用凭据存储在 gitignored .env
文件中。当您部署您的应用时,您应该选择一种安全的方式来存储凭据,并更新您的代码以相应地获取值。例如,您可以使用像 Azure 密钥保管库 这样的密钥管理服务来存储凭据。当您的应用运行时,它可以检索凭据并将它们存储在您的应用部署的服务器上的环境变量中。
有关更多信息,请参阅 "创建 GitHub 应用的最佳实践。"。
分享您的应用
如果您想与其他用户和组织共享您的应用,请将您的应用设为公开。有关更多信息,请参阅 "将 GitHub 应用设为公开或私有。"。
遵循最佳实践
您应该努力遵循 GitHub 应用的最佳实践。有关更多信息,请参阅 "创建 GitHub 应用的最佳实践。"。