简介
本教程演示如何构建一个持续集成 (CI) 服务器,该服务器在推送到存储库的新代码上运行测试。本教程展示了如何构建和配置一个 GitHub 应用,使其充当服务器,使用 GitHub 的 REST API 接收和响应check_run
和check_suite
webhook 事件。
在本教程中,您将使用您的计算机或代码空间作为服务器,同时开发您的应用。一旦应用准备好投入生产使用,您应该将您的应用部署到专用的服务器。
本教程使用 Ruby,但您可以使用可以在服务器上运行的任何编程语言。
本教程分为两部分
- 在第一部分中,您将学习如何使用 GitHub 的 REST API 设置 CI 服务器的框架,在存储库收到新推送的提交时为 CI 测试创建新的检查运行,以及在用户在 GitHub 上请求该操作时重新运行检查运行。
- 在第二部分中,您将通过向 CI 服务器添加 lint 测试来为 CI 测试添加功能。您还将创建显示在拉取请求的“检查”和“已更改的文件”选项卡中的注释,并通过在拉取请求的“检查”选项卡中公开“修复此问题”按钮来自动修复 lint 建议。
关于持续集成 (CI)
CI 是一种软件实践,它要求频繁地将代码提交到共享存储库。更频繁地提交代码可以更快地发现错误,并减少开发人员在查找错误根源时需要调试的代码量。频繁的代码更新还可以更容易地合并来自软件开发团队中不同成员的更改。这对开发人员来说非常棒,他们可以花更多时间编写代码,而花更少时间调试错误或解决合并冲突。
CI 服务器托管运行 CI 测试的代码,例如代码 lint 工具(检查样式格式)、安全检查、代码覆盖率以及针对存储库中新代码提交的其他检查。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 代码空间。
-
在终端中,导航到存储克隆的目录。
-
创建一个名为
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 事件转发到您的计算机或代码空间。本教程使用 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。
-
在本教程中,跳过“识别和授权用户”和“安装后”部分。
-
确保在“Webhook”下选择了活动。
-
在“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 调用之前,您需要初始化一个以 GitHub 应用身份进行身份验证的 Octokit 客户端,使用 authenticate_app
辅助方法。
# 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 应用安装的 ID
- 选项(哈希,默认为
{}
):一组可自定义的选项
每当 GitHub 应用收到 Webhook 时,它都会包含一个带有 id
的 installation
对象。使用以 GitHub 应用身份进行身份验证的客户端,您可以将此 ID 传递给 create_app_installation_access_token
方法以生成每个安装的访问令牌。由于您没有向方法传递任何选项,因此选项默认为空哈希。create_app_installation_access_token
的响应包含两个字段:token
和 expired_at
。模板代码选择响应中的令牌并初始化安装客户端。
使用此方法,每次您的应用收到新的 Webhook 有效负载时,它都会为触发该事件的安装创建客户端。此身份验证过程使您的 GitHub 应用能够适用于任何帐户上的所有安装。
启动服务器
您的应用目前还没有执行任何操作,但此时,您可以让它在服务器上运行。
-
在您的终端中,确保 Smee 仍在运行。有关更多信息,请参阅“获取 Webhook 代理 URL”。
-
在您的终端中打开一个新选项卡,然后
cd
到您在教程前面克隆的存储库所在的目录。有关更多信息,请参阅“创建存储库以存储您的 GitHub 应用的代码”。此存储库中的 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 将 Webhook 有效负载作为 POST
请求发送。因为您已将 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_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
-
在您在“测试服务器是否正在监听您的应用”中创建的测试存储库中创建一个拉取请求。这是您授予应用访问权限的存储库。
-
在您刚刚创建的拉取请求中,导航到检查选项卡。您应该会看到一个“重新运行所有”按钮。
-
单击右上角的“重新运行所有”按钮。测试应再次运行,并以
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
上面的命令通过 HTTPS 克隆存储库。它需要完整的存储库名称,包括存储库所有者(用户或组织)和存储库名称。例如,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 代码风格检查器 和 检查注释。
首先,你将添加代码来运行 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
以机器可解析的格式保存了 lint 结果的副本。有关更多信息以及 JSON 格式的示例,请参阅 RuboCop 文档中的“JSON 格式化程序”。此代码还会解析 JSON,以便你可以使用 @output
变量轻松访问 GitHub 应用程序中的键和值。
运行 RuboCop 并保存 lint 结果后,此代码运行命令 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
文件的存储库中,创建一个新的拉取请求。 -
在服务器正在运行的终端选项卡中,你应该会看到包含 lint 错误的调试输出。lint 错误将以不带任何格式的方式打印。你可以将调试输出复制粘贴到像 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
,以防止严格执行代码风格检查器的错误。但如果要确保在有 lint 错误时检查套件失败,可以将结论更改为 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 分支的新分支的拉取请求。
部署您的应用程序
本教程演示了如何在本地开发应用程序。当您准备好部署应用程序时,您需要进行更改以服务您的应用程序并保持应用程序的凭据安全。您采取的步骤取决于您使用的服务器,但以下部分提供了通用指南。
在服务器上托管您的应用程序
本教程使用您的计算机或 codespace 作为服务器。应用程序准备好投入生产使用后,您应该将应用程序部署到专用服务器。例如,您可以使用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 应用的最佳实践”。