Skip to main content

使用 GitHub 应用构建 CI 检查

使用 GitHub App 构建运行测试的持续集成服务器和检查。

简介

本教程演示如何构建持续集成 (CI) 服务器,以便对推送到存储库的新代码运行测试。 本教程介绍如何使用 GitHub 的 REST API 构建和配置 GitHub App 以充当接收和响应 check_runcheck_suite Webhook 事件的服务器。

在本教程中,你将在开发应用时使用计算机或 codespace 作为服务器。 应用可供生产使用后,应将应用部署到专用服务器。

本教程使用 Ruby,但你可以使用可在服务器上运行的任何编程语言。

本教程分为两个部分:

  • 第一部分介绍如何使用 GitHub 的 REST API 为 CI 服务器设置框架,在存储库收到新推送的提交时为 CI 测试创建新的检查运行,并在用户请求对 GitHub 执行该操作时重新运行检查运行。
  • 第二部分介绍如何通过向 CI 服务器添加 Linter 测试来向 CI 测试添加功能。 你还将创建显示在拉取请求的“检查”和“文件已更改”选项卡中的注释,并通过在拉取请求的“检查”选项卡中显示“修复此问题”按钮来自动修复 Linter 建议 。

关于持续集成 (CI)

CI 是一种需要频繁提交代码到共享仓库的软件实践。 频繁提交代码能较早检测到错误,减少在查找错误来源时开发者需要调试的代码量。 频繁的代码更新也更便于从软件开发团队的不同成员合并更改。 这对开发者非常有益,他们可以将更多时间用于编写代码,而减少在调试错误或解决合并冲突上所花的时间。

CI 服务器托管运行 CI 测试的代码,如代码语法检查(检查样式格式)、安全检查、代码覆盖率以及针对仓库中新代码提交的其他检查。 CI 服务器甚至可以构建代码并将其部署到暂存或生产服务器。 有关可以使用 GitHub App 创建的 CI 测试类型的示例,请参阅 GitHub Marketplace 中提供的持续集成应用

关于检查

GitHub 的 REST API 允许你设置针对存储库中的每个代码提交自动运行的 CI 测试(检查)。 API 在 GitHub 上拉取请求的“检查”选项卡中报告每个检查的详细信息。 你可以使用存储库中的检查来确定代码提交何时引入错误。

检查包括检查运行、检查套件和提交状态。

  • 检查运行是在提交时运行的单个 CI 测试。
  • 检查套件是一组检查运行。
  • 提交状态标记提交的状态,例如 errorfailurependingsuccess,并显示在 GitHub 上的拉取请求中。 检查套件和检查运行都包含提交状态。

GitHub 使用默认流自动为存储库中的新代码提交创建 check_suite 事件,但你可以更改默认设置。 有关详细信息,请参阅“检查套件的 REST API 终结点”。 以下是默认流程的工作方式:

  1. 当用户将代码推送到存储库时,GitHub 会自动将具有 requested 操作的 check_suite 事件发送到存储库上安装的具有 checks:write 权限的所有 GitHub Apps。 此事件让应用知道代码已推送到存储库,并且 GitHub 已自动创建新的检查套件。
  2. 当应用收到此事件时,它可以向该套件添加检查运行。
  3. 检查运行可能包括在特定代码行中显示的注释。 注释显示在“检查”选项卡中。当你为拉取请求中的文件创建注释时,注释也会显示在“文件已更改”选项卡中。有关详细信息,请参阅“检查运行的 REST API 终结点”文档中的 annotations 对象。

有关检查的详细信息,请参阅“检查的 REST API 终结点”和“使用 REST API 与检查交互”。

先决条件

本教程假定你已基本了解 Ruby 编程语言

在开始之前,你可能需要熟悉以下概念:

检查也可与 GraphQL API 一起使用,但本教程重点介绍 REST API。 有关 GraphQL 对象的详细信息,请参阅 GraphQL 文档中的检查套件检查运行

设置

以下部分将引导你设置以下组成部分:

  • 用于存储应用代码的存储库。
  • 用于在本地接收 Webhook 的方式。
  • 订阅了“检查套件”和“检查运行”Webhook 事件的 GitHub App 具有检查的写入权限,并使用可在本地接收的 Webhook URL。

创建存储库以存储 GitHub App 的代码

  1. 创建存储库来存储应用的代码。 有关详细信息,请参阅“创建新仓库”。

  2. 克隆上一步中的存储库。 有关详细信息,请参阅“克隆仓库”。 可以使用本地克隆或 GitHub Codespaces。

  3. 在终端中,导航到存储克隆的目录。

  4. 创建名为 server.rb 的 Ruby 文件。 此文件将包含应用的所有代码。 稍后会向此文件添加内容。

  5. 如果目录尚未包含 .gitignore 文件,请添加 .gitignore 文件。 稍后会向此文件添加内容。 有关 .gitignore 文件的详细信息,请参阅“忽略文件”。

  6. 创建名为 Gemfile 的文件。 此文件将描述 Ruby 代码所需的 gem 依赖项。 将以下内容添加到 Gemfile

    Ruby
    source 'http://rubygems.org'
    
    gem 'sinatra', '~> 2.0'
    gem 'jwt', '~> 2.1'
    gem 'octokit', '~> 4.0'
    gem 'puma'
    gem 'rubocop'
    gem 'dotenv'
    gem 'git'
    
  7. 创建名为 config.ru 的文件。 此文件会将 Sinatra 服务器配置为运行。 将以下内容添加到 config.ru 文件:

    Ruby
    require './server'
    run GHAapp
    

获取 Webhook 代理 URL

若要在本地开发应用,可以使用 Webhook 代理 URL 将 Webhook 事件从 GitHub 转发到你的计算机或 codespace。 本教程使用 Smee.io 来提供 Webhook 代理 URL 并转发事件。

  1. 在终端中,运行以下命令以安装 Smee 客户端:

    Shell
    npm install --global smee-client
    
  2. 在浏览器中,导航到 https://smee.io/。

  3. 单击“启动新频道”。

  4. 复制“Webhook 代理 URL”下的完整 URL。

  5. 在终端中,运行以下命令以启动 Smee 客户端。 将 YOUR_DOMAIN 替换为你在上一步骤中复制的 Web 代理 URL。

    Shell
    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,你的计算机不需要向公共 Internet 开放即可从 GitHub 接收 Webhook。 您也可以在浏览器中打开 Smee URL 来检查 web 挂钩有效负载。

建议你在完成本指南其余步骤时保持此终端窗口打开并保持连接 Smee。 虽然可以断开并重新连接 Smee 客户端而不会丢失唯一的域,但你可能会发现保持连接状态并在不同的终端窗口中执行其他命令行任务更简单。

注册 GitHub App

对于本教程,必须注册一个满足以下条件的 GitHub App:

  • Webhook 处于活动状态
  • 使用可在本地接收的 Webhook URL
  • 具有“检查”存储库权限
  • 订阅“检查套件”和“检查运行”Webhook 事件

以下步骤将引导你使用这些设置来配置 GitHub App。 有关 GitHub App 设置的详细信息,请参阅“注册 GitHub 应用”。

  1. 在 GitHub 上任意页的右上角,单击你的个人资料照片。
  2. 导航到你的帐户设置。
    • 对于由个人帐户拥有的应用,请单击“设置”****。
    • 对于组织拥有的应用:
      1. 单击“你的组织”。
      2. 在组织右侧,单击“设置”。
  3. 在左侧边栏中,单击“ 开发人员设置”。
  4. 在左侧边栏中,单击“GitHub Apps”。
  5. 单击“新建 GitHub 应用”。
  6. 在“GitHub 应用名称”下,为应用输入名称。 例如 USERNAME-ci-test-app,其中 USERNAME 是 GitHub 用户名。
  7. 在“主页 URL”下,输入应用的 URL。 例如,可以使用创建用于存储应用代码的存储库的 URL。
  8. 跳过本教程的“标识和授权用户”和“安装后”部分。
  9. 确保在“Webhook”下选择“活动”。
  10. 在“Webhook URL”下,输入前面提到的 Webhook 代理 URL。 有关详细信息,请参阅“获取 Webhook 代理 URL”。
  11. 在“Webhook 机密”下,输入一个随机字符串。 此机密用于验证 Webhook 是否由 GitHub 发送。 保存此字符串,稍后会用到它。
  12. 在“存储库权限”下的“检查”旁边,选择“读取和写入”。
  13. 在“订阅事件”下,选择“检查套件”和“检查运行” 。
  14. 在“此 GitHub 应用可以安装在哪些位置?”下,选择“仅在此帐户上”。 如果想要发布应用,稍后可以更改此设置。
  15. 单击“创建 GitHub 应用”。

存储应用的标识信息和凭据

本教程介绍如何将应用的凭据和标识信息存储为 .env 文件中的环境变量。 部署应用时,应更改凭据的存储方式。 有关详细信息,请参阅“部署你的应用”。

在执行这些步骤之前,请确保你位于安全的计算机上,因为你将在本地存储凭据。

  1. 在终端中,导航到存储克隆的目录。

  2. 在此目录的顶级创建名为 .env 的文件。

  3. .env 添加到 .gitignore 文件。 这可以防止意外提交应用的凭据。

  4. 将以下内容添加到 .env 文件。 将在后面的步骤中更新值。

    Shell
    GITHUB_APP_IDENTIFIER="YOUR_APP_ID"
    GITHUB_WEBHOOK_SECRET="YOUR_WEBHOOK_SECRET"
    GITHUB_PRIVATE_KEY="YOUR_PRIVATE_KEY"
    
  5. 导航到应用的设置页面:

    1. 在 GitHub 上任意页的右上角,单击你的个人资料照片。

    2. 导航到你的帐户设置。

      • 对于由个人帐户拥有的应用,请单击“设置”****。
      • 对于组织拥有的应用:
        1. 单击“你的组织”。
        2. 在组织右侧,单击“设置”。
    3. 在左侧边栏中,单击“ 开发人员设置”。

    4. 在左侧边栏中,单击“GitHub Apps”。

    5. 在应用名称旁边,单击“编辑”。

  6. 在应用的设置页上,在“应用 ID”旁边,找到应用的应用 ID。

  7. .env 文件中,将 YOUR_APP_ID 替换为应用的应用 ID。

  8. .env 文件中,将 YOUR_WEBHOOK_SECRET 替换为应用的 Webhook 机密。 如果忘记了 Webhook 机密,请在“Webhook 机密(可选)”下单击“更改机密”。 输入新机密,然后单击“保存更改”。

  9. 在应用的设置页上,在“私钥”下,单击“生成私钥”。 你将看到下载到计算机的私钥 .pem 文件。

  10. 使用文本编辑器打开 .pem 文件,或在命令行上使用以下命令显示文件的内容:cat PATH/TO/YOUR/private-key.pem

  11. 将文件的全部内容复制粘贴到 .env 文件中作为 GITHUB_PRIVATE_KEY 的值,并在整个值两边添加双引号。

    下面是示例 .env 文件:

    GITHUB_APP_IDENTIFIER=12345
    GITHUB_WEBHOOK_SECRET=your webhook secret
    GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
    ...
    HkVN9...
    ...
    -----END RSA PRIVATE KEY-----"
    

为 GitHub App 添加代码

本节将介绍如何为 GitHub App 添加一些基本模板代码,并说明代码的作用。 本教程稍后将介绍如何修改并添加到此代码,以构建应用的功能。

将以下模板代码添加到 server.rb 文件:

Ruby
require 'sinatra/base'  # Use the Sinatra web framework
require 'octokit'       # Use the Octokit Ruby library to interact with GitHub's REST API
require 'dotenv/load'   # Manages environment variables
require 'json'          # Allows your app to manipulate JSON data
require 'openssl'       # Verifies the webhook signature
require 'jwt'           # Authenticates a GitHub App
require 'time'          # Gets ISO 8601 representation of a Time object
require 'logger'        # Logs debug statements

# This code is a Sinatra app, for two reasons:
#   1. Because the app will require a landing page for installation.
#   2. To easily handle webhook events.

class GHAapp < Sinatra::Application

  # Sets the port that's used when starting the web server.
  set :port, 3000
  set :bind, '0.0.0.0'

  # Expects the private key in PEM format. Converts the newlines.
  PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))

  # Your registered app must have a webhook secret.
  # The secret is used to verify that webhooks are sent by GitHub.
  WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']

  # The GitHub App's identifier (type integer).
  APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']

  # Turn on Sinatra's verbose logging during development
  configure :development do
    set :logging, Logger::DEBUG
  end

  # Executed before each request to the `/event_handler` route
  before '/event_handler' do
    get_payload_request(request)
    verify_webhook_signature

    # If a repository name is provided in the webhook, validate that
    # it consists only of latin alphabetic characters, `-`, and `_`.
    unless @payload['repository'].nil?
      halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil?
    end

    authenticate_app
    # Authenticate the app installation in order to run API operations
    authenticate_installation(@payload)
  end

  post '/event_handler' do

    # ADD EVENT HANDLING HERE #

    200 # success status
  end

  helpers do

    # ADD CREATE_CHECK_RUN HELPER METHOD HERE #

    # ADD INITIATE_CHECK_RUN HELPER METHOD HERE #

    # ADD CLONE_REPOSITORY HELPER METHOD HERE #

    # ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #

    # Saves the raw payload and converts the payload to JSON format
    def get_payload_request(request)
      # request.body is an IO or StringIO object
      # Rewind in case someone already read it
      request.body.rewind
      # The raw text of the body is required for webhook signature verification
      @payload_raw = request.body.read
      begin
        @payload = JSON.parse @payload_raw
      rescue => e
        fail  'Invalid JSON (#{e}): #{@payload_raw}'
      end
    end

    # Instantiate an Octokit client authenticated as a GitHub App.
    # GitHub App authentication requires that you construct a
    # JWT (https://jwt.io/introduction/) signed with the app's private key,
    # so GitHub can be sure that it came from the app and not altered by
    # a malicious third party.
    def authenticate_app
      payload = {
          # The time that this JWT was issued, _i.e._ now.
          iat: Time.now.to_i,

          # JWT expiration time (10 minute maximum)
          exp: Time.now.to_i + (10 * 60),

          # Your GitHub App's identifier number
          iss: APP_IDENTIFIER
      }

      # Cryptographically sign the JWT.
      jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')

      # Create the Octokit client, using the JWT as the auth token.
      @app_client ||= Octokit::Client.new(bearer_token: jwt)
    end

    # Instantiate an Octokit client, authenticated as an installation of a
    # GitHub App, to run API operations.
    def authenticate_installation(payload)
      @installation_id = payload['installation']['id']
      @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token]
      @installation_client = Octokit::Client.new(bearer_token: @installation_token)
    end

    # Check X-Hub-Signature to confirm that this webhook was generated by
    # GitHub, and not a malicious third party.
    #
    # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to
    # create the hash signature sent in the `X-HUB-Signature` header of each
    # webhook. This code computes the expected hash signature and compares it to
    # the signature sent in the `X-HUB-Signature` header. If they don't match,
    # this request is an attack, and you should reject it. GitHub uses the HMAC
    # hexdigest to compute the signature. The `X-HUB-Signature` looks something
    # like this: 'sha1=123456'.
    def verify_webhook_signature
      their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
      method, their_digest = their_signature_header.split('=')
      our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
      halt 401 unless their_digest == our_digest

      # The X-GITHUB-EVENT header provides the name of the event.
      # The action value indicates the which action triggered the event.
      logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
      logger.debug "----    action #{@payload['action']}" unless @payload['action'].nil?
    end

  end

  # Finally some logic to let us run this server directly from the command line,
  # or with Rack. Don't worry too much about this code. But, for the curious:
  # $0 is the executed file
  # __FILE__ is the current file
  # If they are the same—that is, we are running this file directly, call the
  # Sinatra run method
  run! if __FILE__ == $0
end

本节的其余部分说明模板代码的作用。 在本节中,您不需要完成任何步骤。 如果你已经熟悉模板代码,可以直接跳到“启动服务器”。

了解模板代码

在文本编辑器中打开 server.rb 文件。 你将在整个文件中看到注释,这些注释为模板代码提供了其他上下文。 我们建议您仔细阅读这些注释,甚至为您编写的新代码添加自己的注释。

在所需文件列表下方,你将看到的第一个代码是 class GHApp < Sinatra::Application 声明。 你将在此类中编写 GitHub App 的所有代码。 以下各节详细介绍了代码在此类中执行的操作。

设置端口

你将在 class GHApp < Sinatra::Application 声明中看到的第一个内容是 set :port 3000。 这会设置启动 Web 服务器时使用的端口,以匹配你在“获取 Webhook 代理 URL”中将 Webhook 有效负载重定向到的端口。

  # Sets the port that's used when starting the web server.
  set :port, 3000
  set :bind, '0.0.0.0'

读取环境变量

接下来,此类读取你在“存储应用的标识信息和凭据”中设置的三个环境变量,并将它们存储在变量中供以后使用。

# 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_requestverify_webhook_signatureauthenticate_appauthenticate_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 代码块定义其中每个帮助程序方法。

处理 web 挂钩有效负载

第一个帮助程序方法 get_payload_request 捕获 Webhook 有效负载并将其转换为 JSON 格式,以简化有效负载数据的访问。

验证 web 挂钩签名

第二个帮助程序方法 verify_webhook_signature 执行 Webhook 签名验证,以确保 GitHub 生成了事件。 要详细了解 verify_webhook_signature 帮助程序方法中的代码,请参阅“验证 Webhook 交付。” 如果 Webhook 安全,此方法会将所有传入的有效负载记录到你的终端。 记录器代码有助于验证你的 Web 服务器是否正常工作。

验证为 GitHub App

第三个帮助程序方法 authenticate_app 允许 GitHub App 进行身份验证,以便请求安装令牌。

若要进行 API 调用,你将使用 Octokit 库。 使用此库执行任何有趣的操作都需要 GitHub App 进行身份验证。 有关 Octokit 库的详细信息,请参阅 Octokit 文档

GitHub Apps 有三种身份验证方法:

  • 使用 JSON Web 令牌 (JWT) 以 GitHub App 进行身份验证。
  • 使用安装访问令牌以 GitHub App 的特定安装进行身份验证。
  • 代表用户进行身份验证。 本教程不会使用此身份验证方法。

你将在下一节“以安装进行身份验证”中了解如何以安装进行身份验证。

通过验证为 GitHub App,您可以执行以下操作:

  • 检索关于您的 GitHub App 的高级管理信息。
  • 为应用程序安装申请访问令牌。

例如,可以 GitHub App 进行身份验证,以检索已安装应用的帐户(组织和个人)的列表。 不过这种身份验证方法不允许您使用 API 做很多事情。 要访问仓库的数据并代表安装设施执行操作,您需要验证为安装设施。 为此,你需要先以 GitHub App 进行身份验证才能请求安装访问令牌。 有关详细信息,请参阅“关于使用 GitHub 应用进行身份验证”。

在使用 Octokit.rb 库进行 API 调用之前,需要使用 authenticate_app 帮助程序方法初始化以 GitHub App 进行身份验证的 Octokit 客户端

# Instantiate an Octokit client authenticated as a GitHub App.
# GitHub App authentication requires that you construct a
# JWT (https://jwt.io/introduction/) signed with the app's private key,
# so GitHub can be sure that it came from the app an not altered by
# a malicious third party.
def authenticate_app
  payload = {
      # The time that this JWT was issued, _i.e._ now.
      iat: Time.now.to_i,

      # JWT expiration time (10 minute maximum)
      exp: Time.now.to_i + (10 * 60),

      # Your GitHub App's identifier number
      iss: APP_IDENTIFIER
  }

  # Cryptographically sign the JWT
  jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')

  # Create the Octokit client, using the JWT as the auth token.
  @app_client ||= Octokit::Client.new(bearer_token: jwt)
end

上面的代码生成一个 JSON Web 令牌 (JWT),并使用它(连同应用的私钥)来初始化 Octokit 客户端。 GitHub 通过使用应用程序存储的公钥验证令牌来检查请求的身份验证。 若要详细了解此代码的工作原理,请参阅“为 GitHub 应用生成 JSON Web 令牌 (JWT)”。

验证为安装

第四个和最后一个帮助程序方法 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”。

此方法接受两个参数:

  • installation(整数):GitHub App 安装的 ID
  • 选项(哈希,默认为 {}):一组可自定义的选项

每当 GitHub App 收到 Webhook 时,它都包含一个具有 idinstallation 对象。 使用以 GitHub App 进行身份验证的客户端,将此 ID 传递给 create_app_installation_access_token 方法,以便为每个安装生成访问令牌。 由于您没有向该方法传递任何选项,因此这些选项默认为空哈希。 create_app_installation_access_token 的响应包括两个字段:tokenexpired_at。 模板代码选择响应中的令牌并初始化安装客户端。

使用此方法,您的应用程序每次收到新的 web 挂钩有效负载时,都会为触发事件的安装设施创建一个客户端。 此身份验证过程使 GitHub App 适用于任何帐户上的所有安装。

启动服务器

应用还没有执行任何操作,但是现在你可以让它在服务器上运行。

  1. 在终端中,确保 Smee 仍在运行。 有关详细信息,请参阅“获取 Webhook 代理 URL”。

  2. 在终端中打开一个新选项卡,然后 cd 到克隆你早前在本教程中创建的存储库的目录中。 有关详细信息,请参阅“创建存储库以存储 GitHub 应用的代码”。 此存储库中的 Ruby 代码将启动 Sinatra Web 服务器。

  3. 通过逐个运行以下两个命令来安装依赖项:

    Shell
    gem install bundler
    
    Shell
    bundle install
    
  4. 安装依赖项后,运行以下命令来启动服务器:

    Shell
    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 文件。

  5. 若要测试服务器,请在浏览器中导航到 http://localhost:3000

    如果看到一个错误页面,显示“Sinatra 不知道此小问题”,则表明该应用正常工作。 即使它是错误页面,它也是一个 Sinatra 错误页面,这意味着你的应用已按预期连接到服务器。 之所以会看到此消息,是因为您还没有给应用程序提供任何要显示的内容。

测试服务器是否正在侦听应用

您可以通过触发要接收的事件来测试服务器是否正在侦听您的应用程序。 为此,你将在测试存储库上安装应用,该存储库会将 installation 事件发送到应用。 如果应用收到它,你应该会在运行 server.rb 的终端选项卡中看到输出。

  1. 创建一个新存储库用于测试教程代码。 有关详细信息,请参阅“创建新仓库”。

  2. 在刚刚创建的存储库上安装 GitHub App。 有关详细信息,请参阅“安装自己的 GitHub 应用”。 在安装过程中,选择“仅选择存储库”,然后选择在上一步中创建的存储库。

  3. 单击“安装”后,在运行 server.rb 的终端选项卡中查看输出。 你应看到与下面类似的内容:

    > D, [2023-06-08T15:45:43.773077 #30488] DEBUG -- : ---- received event installation
    > D, [2023-06-08T15:45:43.773141 #30488]] DEBUG -- : ----    action created
    > 192.30.252.44 - - [08/Jun/2023:15:45:43 -0400] "POST /event_handler HTTP/1.1" 200 - 0.5390
    

    如果你看到这样的输出,则表示你的应用收到了一条通知,指出它已安装在你的 GitHub 帐户上。 应用在服务器上正常运行。

    如果没有看到此输出,请确保 Smee 在另一个终端选项卡中正常运行。如果需要重启 Smee,请注意还需要卸载并重新安装该应用以再次将 installation 事件发送到你的应用并在终端中看到输出 。

如果你想知道上述终端输出来自何处,它在你在“为 GitHub App 添加代码”中添加到 server.rb 的应用模板代码中编写。

第 1 部分。 创建检查 API 接口

在本部分,你将添加必要的代码,以接收 check_suite Webhook 事件并创建和更新检查运行。 你还将了解如何在 GitHub 上重新请求检查时创建检查运行。 在本节的最后,你将能够查看在 GitHub 拉取请求中创建的检查运行。

你的检查运行不会对本节中的代码执行任何检查。 你将在“第 2 部分:创建 CI 测试”中添加该功能。

您应该已经配置了可将 web 挂钩有效负载转发到本地服务器的 Smee 通道。 你的服务器应该正在运行并连接到你在测试存储库上注册并安装的 GitHub App。

以下是您将在第 1 部分中完成的步骤:

  1. 添加事件处理
  2. 创建检查运行
  3. 更新检查运行

步骤 1.1. 添加事件处理

由于你的应用订阅了“检查套件”和“检查运行”事件,它将接收 check_suitecheck_run Webhook 。 GitHub 将 Webhook 有效负载作为 POST 请求发送。 因为你已将 Smee Webhook 有效负载转发到 http://localhost:3000/event_handler,因此服务器将在 post '/event_handler' 路由中接收 POST 请求有效负载。

打开在“为 GitHub App 添加代码”中创建的 server.rb 文件,并查找以下代码。 模板代码中已包含一个空 post '/event_handler' 路由。 空路由如下所示:

  post '/event_handler' do

    # ADD EVENT HANDLING HERE #

    200 # success status
  end

在以 post '/event_handler' do 开头的代码块(显示 # ADD EVENT HANDLING HERE #)中,添加以下代码。 此路由将处理 check_suite 事件。

Ruby
    # 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_suiteaction 字段可以是 requestedrerequestedcompleted

每当有代码推送到存储库时,requested 操作会请求检查运行,而 rerequested 操作则请求你对存储库中已经存在的代码重新运行检查。 由于 requestedrerequested 操作都需要创建检查运行,因此你将调用名为 create_check_run 的帮助程序。 现在我们来编写该方法。

步骤 1.2. 创建检查运行

如果希望其他路由也使用此新方法,请将其添加为 Sinatra 帮助程序

在以 helpers do 开头的代码块(显示 # ADD CREATE_CHECK_RUN HELPER METHOD HERE #)中,添加以下代码:

Ruby
    # 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 终结点”。

若要创建检查运行,只需要两个输入参数:namehead_sha。 在此代码中,我们将检查运行命名为“Octo RuboCop”,因为我们将在本教程的后面部分使用 RuboCop 实现 CI 测试。 但你可以为检查运行选择所需的任何名称。 有关 RuboCop 的详细信息,请参阅 RuboCop 文档

您现在仅提供必需的参数以使基本功能正常工作,但是稍后您将在收集有关检查运行的更多信息时更新检查运行。 默认情况下,GitHub 将 status 设置为 queued

GitHub 为特定的提交 SHA 创建检查运行,因此 head_sha 是必需参数。 您可以在 web 挂钩有效负载中找到提交 SHA。 虽然现在你只为 check_suite 事件创建检查运行,但最好知道 head_sha 包含在事件有效负载中的 check_suitecheck_run 对象中。

上面的代码使用三元运算符(其工作原理类似于 if/else 语句)来检查有效负载是否包含 check_run 对象。 如果包含,你将从 check_run 对象中读取 head_sha,否则从 check_suite 对象中读取。

测试代码

以下步骤将向你展示如何测试代码是否正常工作,以及它是否成功创建新的检查运行。

  1. 运行以下命令,从终端重启服务器。 如果服务器已在运行,请先在终端中输入 Ctrl-C 以停止服务器,然后运行以下命令以再次启动服务器。

    Shell
    ruby server.rb
    
  2. 在“测试服务器是否正在侦听应用”中创建的测试存储库中创建拉取请求。 这是你授予应用访问权限的存储库。

  3. 在你刚刚创建的拉取请求中,导航到“检查”选项卡。你应该会看到一个检查运行,其名称为“Octo RuboCop”或之前为该检查运行选择的名称。

如果在“检查”选项卡中看到其他应用,这意味着你已在存储库上安装了其他应用,这些应用对检查具有“读取和写入”访问权限,并订阅了“检查套件”和“检查运行”事件 。 这可能还意味着存储库上有 GitHub Actions 工作流,这些工作流由 pull_requestpull_request_target 事件触发。

到目前为止,你已告知 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 #)中,添加以下代码:

Ruby
    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 检查运行与 requestedrerequested 检查套件略有不同,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 #)中,添加以下代码:

Ruby
    # 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 时,需要 conclusioncompleted_at 参数。 conclusion 汇总了检查运行的结果,可以是 successfailureneutralcancelledtimed_outskippedaction_required。 你要将结论设置为 success、将 completed_at 时间设置为当前时间并将状态设置为 completed

您还可以提供有关检查操作的更多详细信息,但这些内容将在下一部分进行介绍。

测试代码

以下步骤将向你展示如何测试代码是否正常工作,以及你创建的新“全部重新运行”按钮是否正常工作。

  1. 运行以下命令,从终端重启服务器。 如果服务器已在运行,请先在终端中输入 Ctrl-C 以停止服务器,然后运行以下命令以再次启动服务器。

    Shell
    ruby server.rb
    
  2. 在“测试服务器是否正在侦听应用”中创建的测试存储库中创建拉取请求。 这是你授予应用访问权限的存储库。

  3. 在刚刚创建的拉取请求中,导航到“检查”选项卡。你应该会看到“全部重新运行”按钮。

  4. 单击右上角的“全部重新运行”按钮。 测试应再次运行,以 success 结束。

第 2 部分。 创建 CI 测试

你已经创建了用于接收 API 事件和创建检查运行的接口,现在可以创建实现 CI 测试的检查运行。

RuboCop 是一个 Ruby 代码 Linter 和格式化程序。 它会检查 Ruby 代码,以确保符合 Ruby 样式指南。 有关详细信息,请参阅 RuboCop 文档

RuboCop 有三个主要功能:

  • 分析检查代码样式
  • 代码格式设置
  • 使用 ruby -w 替换本地 Ruby Lint 分析功能

你的应用将在 CI 服务器上运行 RuboCop,并创建检查运行(本例中为 CI 测试),以报告 RuboCop 向 GitHub 报告的结果。

REST API 允许你报告关于每个检查运行的丰富细节,包括状态、图像、摘要、注释和请求的操作。

注释是关于仓库中特定代码行的信息。 注释允许您精确定位和可视化要显示其他信息的代码确切部分。 例如,可以将该信息显示为特定代码行上的注释、错误或警告。 本教程使用注释来可视化 RuboCop 错误。

为了利用请求的操作,应用开发人员可以在拉取请求的“检查”选项卡中创建按钮。 当用户单击其中一个按钮时,会向 GitHub App 应用发送 requested_action``check_run 事件。 应用程序执行的操作完全由应用程序开发者配置。 本教程将引导你添加一个按钮,以允许用户请求 RuboCop 修复它发现的错误。 RuboCop 支持使用命令行选项自动修复错误,你将配置 requested_action 以利用此选项。

以下是您将在本部分中完成的步骤:

  1. 添加 Ruby 文件
  2. 允许 RuboCop 克隆测试存储库
  3. 运行 RuboCop
  4. 收集 RuboCop 错误
  5. 使用 CI 测试结果更新检查运行
  6. 自动修复 RuboCop 错误

步骤 2.1. 添加 Ruby 文件

您可以传递特定文件或整个目录供 RuboCop 检查。 在本教程中,你将在整个目录上运行 RuboCop。 RuboCop 仅检查 Ruby 代码。 若要测试 GitHub App,需要在存储库中添加一个 Ruby 文件,其中包含 RuboCop 发现的错误。 将以下 Ruby 文件添加到存储库后,你将更新 CI 检查,以便对代码运行 RuboCop。

  1. 导航到你在“测试服务器是否正在侦听应用”中创建的测试存储库。 这是你授予应用访问权限的存储库。

  2. 创建名为 myfile.rb 的新文件。 有关详细信息,请参阅“创建新文件”。

  3. 将以下内容添加到 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
    
  4. 如果在本地创建了文件,请确保提交文件并将其推送到 GitHub 上的存储库。

步骤 2.2. 允许 RuboCop 克隆测试存储库

RuboCop 可用作命令行实用工具。 这意味着,如果要在存储库上运行 RuboCop,你的 GitHub App 需要在 CI 服务器上克隆存储库的本地副本,以便 RuboCop 可以分析文件。 为此,你的代码需要能够运行 Git 操作,并且你的 GitHub App 需要具有正确的权限来克隆存储库。

允许 Git 操作

若要在 Ruby 应用中运行 Git 操作,可以使用 ruby-git gem。 你在“设置”中创建的 Gemfile 已包含 ruby-git gem,你在“启动服务器”中运行 bundle install 时安装了该依赖项。

现在,在 server.rb 文件顶部的其他 require 项下方,添加以下代码:

Ruby
require 'git'

更新应用权限

接下来,需要更新 GitHub App 的权限。 你的应用需要“内容”的读取权限才能克隆存储库。 在本教程的后面部分,将需要写入权限才能将内容推送到 GitHub。 要更新应用程序的权限:

  1. 应用设置页面中选择应用,然后单击边栏中的“权限和事件”。
  2. 在“存储库权限”下的“内容”旁边,选择“读取和写入”。
  3. 单击页面底部的“保存更改”。
  4. 如果您已经在您的帐户上安装了应用程序,请检查您的电子邮件并按照链接接受新的权限。 每次更改应用程序的权限或 web 挂钩时,安装应用程序的用户(包括您自己)都需要在更改生效之前接受新权限。 也可以导航到安装页面来接受新权限。 你将在应用名称下看到一个链接,通过它你可以知道应用正在请求不同的权限。 单击“查看请求”,然后单击“接受新权限”。

添加代码以克隆存储库

若要克隆存储库,代码将使用 GitHub App 的权限和 Octokit SDK 为应用创建安装令牌 (x-access-token:TOKEN) 并在以下克隆命令中使用它:

git clone https://x-access-token:TOKEN@github.com/OWNER/REPO.git

上面的命令通过 HTTP 克隆存储库。 它需要完整的仓库名称,其中包括仓库所有者(用户或组织)和仓库名称。 例如,octocat Hello-World 存储库的全名为 octocat/hello-world

打开 server.rb 文件。 在以 helpers do 开头的代码块(显示 # ADD CLONE_REPOSITORY HELPER METHOD HERE #)中,添加以下代码:

Ruby
    # 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 *****)中,添加以下代码:

Ruby
    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 #)下,添加以下代码:

Ruby
        # 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 App 中的键和值。

运行 RuboCop 并保存 Lint 分析结果后,此代码运行 rm -rf 命令以删除存储库的签出。 由于此代码将 RuboCop 结果存储在 @report 变量中,因此可以安全地删除存储库的签出。

rm -rf 命令无法撤消。 为了确保应用的安全,本教程中的代码会检查传入的 Webhook 中是否存在注入的恶意命令,这些命令可用于删除与应用预期不同的目录。 例如,如果一个恶意行为者发送了一个存储库名称为 ./ 的 Webhook,你的应用将会移除根目录。 verify_webhook_signature 方法验证 Webhook 的发送方。 verify_webhook_signature 事件处理程序还会检查存储库名称是否有效。 有关详细信息,请参阅“定义 before 筛选器”。

测试代码

以下步骤将向你展示如何测试代码是否正常工作并查看 RuboCop 报告的错误。

  1. 运行以下命令,从终端重启服务器。 如果服务器已在运行,请先在终端中输入 Ctrl-C 以停止服务器,然后运行以下命令以再次启动服务器。

    Shell
    ruby server.rb
    
  2. 在添加 myfile.rb 文件的存储库中,创建新的拉取请求。

  3. 在运行服务器的终端选项卡中,你应该会看到包含 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 个注释,第三个请求将包括其余五个注释。 每次更新检查运行时,注释都会添加到已经存在的检查运行注释列表中。

检查运行会将注释作为对象数组。 每个注释对象必须包含 pathstart_lineend_lineannotation_levelmessage。 RuboCop 还提供了 start_columnend_column,因此你可以在注释中包含这些可选参数。 注释仅在同一行中支持 start_columnend_column。 有关详细信息,请参阅“检查运行的 REST API 终结点”中的 annotations 对象。

现在,你将添加代码以从 RuboCop 中提取创建每个注释所需的信息。

在上一步中添加的代码(显示 # ADD ANNOTATIONS CODE HERE #)下,添加以下代码:

Ruby
    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,以防止严格执行来自代码语法检查的错误。 但如果你想确保检查套件在发现分析错误时失败,可以将结论更改为 failure

当报告错误时,上面的代码将循环访问 RuboCop 报告中的 files 数组。 对于每个文件,它会提取文件路径,并将注释级别设置为 notice。 你可以更进一步,为每种类型的 RuboCop Cop 设置特定的警告级别,但本教程为了保持简单,所有错误都设置为 notice 级别。

此代码还会循环访问 offenses 数组中的每个错误,并收集超限的位置和错误消息。 提取所需的信息后,代码将为每个错误创建一个注释,并将其存储在 annotations 数组中。 由于注释仅在同一行上支持起始列和结束列,因此 start_columnend_column 仅在开始和结束行值相同时才会添加到 annotation 对象。

此代码尚未为检查运行创建注释。 您将在下一节中添加该代码。

步骤 2.5. 使用 CI 测试结果更新检查运行

GitHub 中的每个检查运行都包含一个 output,其中包括 titlesummarytextannotationsimagessummarytitleoutput 仅有的必需参数,但仅有这些参数无法提供太多详细信息,因此本教程还添加了 textannotations

对于 summary,此示例使用 RuboCop 中的摘要信息,并添加了换行符 (\n) 来设置输出格式。 你可以自定义要添加到 text 参数的内容,但此示例将 text 参数设置为 RuboCop 版本。 以下代码设置 summarytext

在上一步中添加的代码(显示 # ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #)下,添加以下代码:

Ruby
        # 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 变量(更新为 successneutral)。 下面是你之前添加到 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'
)

将该代码替换为以下代码:

Ruby
        # 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 测试。

  1. 运行以下命令,从终端重启服务器。 如果服务器已在运行,请先在终端中输入 Ctrl-C 以停止服务器,然后运行以下命令以再次启动服务器。

    Shell
    ruby server.rb
    
  2. 在添加 myfile.rb 文件的存储库中,创建新的拉取请求。

  3. 在刚刚创建的拉取请求中,导航到“检查”选项卡。你应该会看到 RuboCop 发现的每个错误的注释。 另请注意通过添加请求的操作创建的“修复此问题”按钮。

步骤 2.6. 自动修复 RuboCop 错误

到目前为止,你已创建 CI 测试。 在本节中,您将添加另外一个功能,即使用 RuboCop 自动修复它发现的错误。 你已在“步骤 2.5. 使用 CI 测试结果更新检查运行”中添加了“修复此问题”按钮。 现在,你将添加代码来处理用户单击“修复此问题”按钮时触发的 requested_action 检查运行事件。

RuboCop 工具提供了 --auto-correct 命令行选项,以自动修复它发现的错误。 有关详细信息,请参阅 RuboCop 文档中的“自动更正违规行为”。 使用 --auto-correct 功能时,更新将应用于服务器上的本地文件。 在 RuboCop 进行修复后,你需要将更改推送到 GitHub。

若要推送到存储库,你的应用必须具备存储库中“内容”的写入权限。 你已在“步骤 2.2. 允许 RuboCop 克隆测试存储库”中将该权限设置回“读取和写入****”。

若要提交文件,Git 必须知道要与提交关联的用户名和电子邮件地址。 接下来,你将添加环境变量来存储应用在进行 Git 提交时将使用的名称和电子邮件地址。

  1. 打开早前在本教程中创建的 .env 文件。

  2. 将以下环境变量添加到 .env 文件。 将 APP_NAME 替换为应用的名称,将 EMAIL_ADDRESS 替换为要用于此示例的任何电子邮件。

    Shell
    GITHUB_APP_USER_NAME="APP_NAME"
    GITHUB_APP_USER_EMAIL="EMAIL_ADDRESS"
    

接下来,需要添加代码来读取环境变量并设置 Git 配置。 您很快就将添加该代码。

当有人单击“修复此问题”按钮时,你的应用会收到包含 requested_action 操作类型的检查运行 Webhook

在“步骤 1.3. 更新检查运行”中,你已更新 server.rb 文件中的 event_handler 以在 check_run 事件中查找操作。 你已经有一个用于处理 createdrerequested 操作类型的 case 语句:

when 'check_run'
  # Check that the event is being sent to this app
  if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER
    case @payload['action']
    when 'created'
      initiate_check_run
    when 'rerequested'
      create_check_run
    # ADD REQUESTED_ACTION METHOD HERE #
  end
end

rerequested case(显示 # ADD REQUESTED_ACTION METHOD HERE #)之后,添加以下代码:

Ruby
    when 'requested_action'
      take_requested_action

此代码调用将处理应用的所有 requested_action 事件的新方法。

在以 helpers do 开头的代码块(显示 # ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #)中,添加以下帮助程序方法:

Ruby
    # Handles the check run `requested_action` event
    # See /webhooks/event-payloads/#check_run
    def take_requested_action
      full_repo_name = @payload['repository']['full_name']
      repository     = @payload['repository']['name']
      head_branch    = @payload['check_run']['check_suite']['head_branch']

      if (@payload['requested_action']['identifier'] == 'fix_rubocop_notices')
        clone_repository(full_repo_name, repository, head_branch)

        # Sets your commit username and email address
        @git.config('user.name', ENV['GITHUB_APP_USER_NAME'])
        @git.config('user.email', ENV['GITHUB_APP_USER_EMAIL'])

        # Automatically correct RuboCop style errors
        @report = `rubocop '#{repository}/*' --format json --auto-correct`

        pwd = Dir.getwd()
        Dir.chdir(repository)
        begin
          @git.commit_all('Automatically fix Octo RuboCop notices.')
          @git.push("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", head_branch)
        rescue
          # Nothing to commit!
          puts 'Nothing to commit'
        end
        Dir.chdir(pwd)
        `rm -rf '#{repository}'`
      end
    end

上面的代码将克隆存储库,就像你在“步骤 2.2. 允许 RuboCop 克隆测试存储库”中添加的代码一样。 if 语句会检查所请求操作的标识符是否与 RuboCop 按钮标识符 (fix_rubocop_notices) 匹配。 如果它们匹配,代码将克隆存储库,设置 Git 用户名和电子邮件,并使用选项 --auto-correct 运行 RuboCop。 --auto-correct 选项会将更改自动应用于本地 CI 服务器文件。

文件在本地更改,但你仍需要将它们推送到 GitHub。 你将使用 ruby-git gem 来提交所有文件。 Git 有一个命令可以暂存所有已修改或删除的文件并提交它们:git commit -a。 若要使用 ruby-git 执行相同操作,上述代码将使用 commit_all 方法。 然后,代码使用与 Git clone 命令相同的身份验证方法,通过安装令牌将提交的文件推送到 GitHub。 最后,它删除仓库目录,以确保为下一个事件准备工作目录。

你编写的代码现已完成使用 GitHub App 构建的持续集成服务器和检查。 若要查看应用的完整最终代码,请参阅“完整代码示例”。

测试代码

以下步骤将向你展示如何测试代码是否正常工作,以及 RuboCop 是否可以自动修复它发现的错误。

  1. 运行以下命令,从终端重启服务器。 如果服务器已在运行,请先在终端中输入 Ctrl-C 以停止服务器,然后运行以下命令以再次启动服务器。

    Shell
    ruby server.rb
    
  2. 在添加 myfile.rb 文件的存储库中,创建新的拉取请求。

  3. 在创建的新拉取请求中,导航到“检查”选项卡,然后单击“修复此问题”按钮自动修复 RuboCop 发现的错误。

  4. 导航到“提交”选项卡。你应该会看到由 Git 配置中设置的用户名进行的新提交。 您可能需要刷新浏览器才能看到更新。

  5. 导航到“检查”选项卡。你应该会看到 Octo RuboCop 的新检查套件。 但这次应该没有任何错误,因为 RuboCop 已经修复了所有错误。

完整代码示例

这是按照本教程中的所有步骤操作后 server.rb 中的最终代码示例。 整个代码中还有一些注释,提供了其他上下文。

Ruby
require 'sinatra/base'  # Use the Sinatra web framework
require 'octokit'       # Use the Octokit Ruby library to interact with GitHub's REST API
require 'dotenv/load'   # Manages environment variables
require 'json'          # Allows your app to manipulate JSON data
require 'openssl'       # Verifies the webhook signature
require 'jwt'           # Authenticates a GitHub App
require 'time'          # Gets ISO 8601 representation of a Time object
require 'logger'        # Logs debug statements

# This code is a Sinatra app, for two reasons:
#   1. Because the app will require a landing page for installation.
#   2. To easily handle webhook events.

class GHAapp < Sinatra::Application

  # Sets the port that's used when starting the web server.
  set :port, 3000
  set :bind, '0.0.0.0'

  # Expects the private key in PEM format. Converts the newlines.
  PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))

  # Your registered app must have a webhook secret.
  # The secret is used to verify that webhooks are sent by GitHub.
  WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']

  # The GitHub App's identifier (type integer).
  APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']

  # Turn on Sinatra's verbose logging during development
  configure :development do
    set :logging, Logger::DEBUG
  end

  # Executed before each request to the `/event_handler` route
  before '/event_handler' do
    get_payload_request(request)
    verify_webhook_signature

    # If a repository name is provided in the webhook, validate that
    # it consists only of latin alphabetic characters, `-`, and `_`.
    unless @payload['repository'].nil?
      halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil?
    end

    authenticate_app
    # Authenticate the app installation in order to run API operations
    authenticate_installation(@payload)
  end

  post '/event_handler' do

    # Get the event type from the HTTP_X_GITHUB_EVENT header
    case request.env['HTTP_X_GITHUB_EVENT']

    when 'check_suite'
      # A new check_suite has been created. Create a new check run with status queued
      if @payload['action'] == 'requested' || @payload['action'] == 'rerequested'
        create_check_run
      end

    when 'check_run'
      # Check that the event is being sent to this app
      if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER
        case @payload['action']
        when 'created'
          initiate_check_run
        when 'rerequested'
          create_check_run
        when 'requested_action'
          take_requested_action
        end
      end
    end

    200 # success status
  end

  helpers do

    # Create a new check run with status "queued"
    def create_check_run
      @installation_client.create_check_run(
        # [String, Integer, Hash, Octokit Repository object] A GitHub repository.
        @payload['repository']['full_name'],
        # [String] The name of your check run.
        'Octo RuboCop',
        # [String] The SHA of the commit to check
        # The payload structure differs depending on whether a check run or a check suite event occurred.
        @payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha'],
        # [Hash] 'Accept' header option, to avoid a warning about the API not being ready for production use.
        accept: 'application/vnd.github+json'
      )
    end

    # Start the CI process
    def initiate_check_run
      # Once the check run is created, you'll update the status of the check run
      # to 'in_progress' and run the CI process. When the CI finishes, you'll
      # update the check run status to 'completed' and add the CI results.

      @installation_client.update_check_run(
        @payload['repository']['full_name'],
        @payload['check_run']['id'],
        status: 'in_progress',
        accept: 'application/vnd.github+json'
      )

      full_repo_name = @payload['repository']['full_name']
      repository     = @payload['repository']['name']
      head_sha       = @payload['check_run']['head_sha']

      clone_repository(full_repo_name, repository, head_sha)

      # Run RuboCop on all files in the repository
      @report = `rubocop '#{repository}' --format json`
      logger.debug @report
      `rm -rf #{repository}`
      @output = JSON.parse @report

      annotations = []
      # You can create a maximum of 50 annotations per request to the Checks
      # API. To add more than 50 annotations, use the "Update a check run" API
      # endpoint. This example code limits the number of annotations to 50.
      # See /rest/reference/checks#update-a-check-run
      # for details.
      max_annotations = 50

      # RuboCop reports the number of errors found in "offense_count"
      if @output['summary']['offense_count'] == 0
        conclusion = 'success'
      else
        conclusion = 'neutral'
        @output['files'].each do |file|

          # Only parse offenses for files in this app's repository
          file_path = file['path'].gsub(/#{repository}\//,'')
          annotation_level = 'notice'

          # Parse each offense to get details and location
          file['offenses'].each do |offense|
            # Limit the number of annotations to 50
            next if max_annotations == 0
            max_annotations -= 1

            start_line   = offense['location']['start_line']
            end_line     = offense['location']['last_line']
            start_column = offense['location']['start_column']
            end_column   = offense['location']['last_column']
            message      = offense['message']

            # Create a new annotation for each error
            annotation = {
              path: file_path,
              start_line: start_line,
              end_line: end_line,
              start_column: start_column,
              end_column: end_column,
              annotation_level: annotation_level,
              message: message
            }
            # Annotations only support start and end columns on the same line
            if start_line == end_line
              annotation.merge({start_column: start_column, end_column: end_column})
            end

            annotations.push(annotation)
          end
        end
      end

      # Updated check run summary and text parameters
      summary = "Octo RuboCop summary\n-Offense count: #{@output['summary']['offense_count']}\n-File count: #{@output['summary']['target_file_count']}\n-Target file count: #{@output['summary']['inspected_file_count']}"
      text = "Octo RuboCop version: #{@output['metadata']['rubocop_version']}"

      # Mark the check run as complete! And if there are warnings, share them.
      @installation_client.update_check_run(
        @payload['repository']['full_name'],
        @payload['check_run']['id'],
        status: 'completed',
        conclusion: conclusion,
        output: {
          title: 'Octo RuboCop',
          summary: summary,
          text: text,
          annotations: annotations
        },
        actions: [{
          label: 'Fix this',
          description: 'Automatically fix all linter notices.',
          identifier: 'fix_rubocop_notices'
        }],
        accept: 'application/vnd.github+json'
      )
    end

    # Clones the repository to the current working directory, updates the
    # contents using Git pull, and checks out the ref.
    #
    # full_repo_name  - The owner and repo. Ex: octocat/hello-world
    # repository      - The repository name
    # ref             - The branch, commit SHA, or tag to check out
    def clone_repository(full_repo_name, repository, ref)
      @git = Git.clone("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", repository)
      pwd = Dir.getwd()
      Dir.chdir(repository)
      @git.pull
      @git.checkout(ref)
      Dir.chdir(pwd)
    end

    # Handles the check run `requested_action` event
    # See /webhooks/event-payloads/#check_run
    def take_requested_action
      full_repo_name = @payload['repository']['full_name']
      repository     = @payload['repository']['name']
      head_branch    = @payload['check_run']['check_suite']['head_branch']

      if (@payload['requested_action']['identifier'] == 'fix_rubocop_notices')
        clone_repository(full_repo_name, repository, head_branch)

        # Sets your commit username and email address
        @git.config('user.name', ENV['GITHUB_APP_USER_NAME'])
        @git.config('user.email', ENV['GITHUB_APP_USER_EMAIL'])

        # Automatically correct RuboCop style errors
        @report = `rubocop '#{repository}/*' --format json --auto-correct`

        pwd = Dir.getwd()
        Dir.chdir(repository)
        begin
          @git.commit_all('Automatically fix Octo RuboCop notices.')
          @git.push("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", head_branch)
        rescue
          # Nothing to commit!
          puts 'Nothing to commit'
        end
        Dir.chdir(pwd)
        `rm -rf '#{repository}'`
      end
    end

    # Saves the raw payload and converts the payload to JSON format
    def get_payload_request(request)
      # request.body is an IO or StringIO object
      # Rewind in case someone already read it
      request.body.rewind
      # The raw text of the body is required for webhook signature verification
      @payload_raw = request.body.read
      begin
        @payload = JSON.parse @payload_raw
      rescue => e
        fail  'Invalid JSON (#{e}): #{@payload_raw}'
      end
    end

    # Instantiate an Octokit client authenticated as a GitHub App.
    # GitHub App authentication requires that you construct a
    # JWT (https://jwt.io/introduction/) signed with the app's private key,
    # so GitHub can be sure that it came from the app and not altered by
    # a malicious third party.
    def authenticate_app
      payload = {
          # The time that this JWT was issued, _i.e._ now.
          iat: Time.now.to_i,

          # JWT expiration time (10 minute maximum)
          exp: Time.now.to_i + (10 * 60),

          # Your GitHub App's identifier number
          iss: APP_IDENTIFIER
      }

      # Cryptographically sign the JWT.
      jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')

      # Create the Octokit client, using the JWT as the auth token.
      @app_client ||= Octokit::Client.new(bearer_token: jwt)
    end

    # Instantiate an Octokit client, authenticated as an installation of a
    # GitHub App, to run API operations.
    def authenticate_installation(payload)
      @installation_id = payload['installation']['id']
      @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token]
      @installation_client = Octokit::Client.new(bearer_token: @installation_token)
    end

    # Check X-Hub-Signature to confirm that this webhook was generated by
    # GitHub, and not a malicious third party.
    #
    # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to
    # create the hash signature sent in the `X-HUB-Signature` header of each
    # webhook. This code computes the expected hash signature and compares it to
    # the signature sent in the `X-HUB-Signature` header. If they don't match,
    # this request is an attack, and you should reject it. GitHub uses the HMAC
    # hexdigest to compute the signature. The `X-HUB-Signature` looks something
    # like this: 'sha1=123456'.
    def verify_webhook_signature
      their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
      method, their_digest = their_signature_header.split('=')
      our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
      halt 401 unless their_digest == our_digest

      # The X-GITHUB-EVENT header provides the name of the event.
      # The action value indicates the which action triggered the event.
      logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
      logger.debug "----    action #{@payload['action']}" unless @payload['action'].nil?
    end

  end

  # Finally some logic to let us run this server directly from the command line,
  # or with Rack. Don't worry too much about this code. But, for the curious:
  # $0 is the executed file
  # __FILE__ is the current file
  # If they are the same—that is, we are running this file directly, call the
  # Sinatra run method
  run! if __FILE__ == $0
end

后续步骤

现在你应该有一个应用,该应用接收 API 事件、创建检查运行、使用 RuboCop 查找 Ruby 错误、在拉取请求中创建注释并自动修复 Linter 错误。 接下来,你可能想要扩展应用的代码、部署应用并公开应用。

如果有任何问题,请在 API 和 Webhook 类别中启动 GitHub Community 讨论

修改应用代码

本教程演示了如何创建始终显示在存储库中的拉取请求中的“修复此问题”按钮。 尝试更新代码以仅在 RuboCop 发现错误时才显示“修复此问题”按钮。

如果你不希望 RuboCop 将文件直接提交到头分支,请更新代码以使用基于头分支的新分支创建拉取请求。

部署你的应用

本教程演示了如何在本地开发应用。 准备好部署应用后,需要进行更改来为应用提供服务,并确保应用的凭据安全。 执行的步骤取决于所使用的服务器,但以下部分提供了一般指导。

在服务器上托管你的应用

本教程使用了计算机或 codespace 作为服务器。 应用可供生产使用后,应将应用部署到专用服务器。 例如,可以使用 Azure 应用服务

更新 Webhook URL

将服务器设置为从 GitHub 接收 Webhook 流量后,请在应用设置中更新 Webhook URL。 不应使用 Smee.io 在生产环境中转发 Webhook。

更新 :port 设置

部署应用时,需要更改服务器侦听的端口。 该代码已经通过将 :bind 设置为 0.0.0.0 来告知服务器侦听所有可用的网络接口。

例如,可以在服务器上的 .env 文件中设置一个 PORT 变量,以指示服务器应侦听的端口。 然后,可以更新代码设置 :port 的位置,以便服务器侦听部署端口:

Ruby
set :port, ENV['PORT']

保护应用的凭据

切勿公开应用的私钥或 Webhook 机密。 本教程将应用的凭据存储在 gitignored .env 文件中。 部署应用时,应选择一种安全的方式来存储凭据并更新代码以获取相应值。 例如,可以使用 Azure 密钥保管库等机密管理服务存储凭据。 应用运行时,它可以检索凭据并将其存储在部署应用的服务器上的环境变量中。

有关详细信息,请参阅“创建 GitHub 应用的最佳做法”。

共享应用

如果要与其他用户和组织共享应用,请公开应用。 有关详细信息,请参阅“将 GitHub 应用程序设为公共或私有”。

遵循最佳做法

你的目标应该是遵循 GitHub App 的最佳做法。 有关详细信息,请参阅“创建 GitHub 应用的最佳做法”。