跳至主要内容

构建响应 Webhook 事件的 GitHub 应用

了解如何构建一个 GitHub 应用,该应用响应 Webhook 事件并发出 API 请求。

简介

本教程演示了如何编写代码来创建 GitHub 应用,该应用响应 Webhook 事件并发出 API 请求。具体来说,当在应用已授予访问权限的存储库中打开拉取请求时,应用将接收一个拉取请求 Webhook 事件。然后,应用将使用 GitHub 的 API 向拉取请求添加注释。

在本教程中,您将使用您的计算机或 Codespace 作为服务器来开发您的应用程序。应用程序准备好投入生产使用后,您应该将其部署到专用的服务器上。

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

关于 Webhook

注册 GitHub 应用程序时,您可以指定一个 Webhook URL 并订阅 Webhook 事件。当 GitHub 上的活动触发您的应用程序订阅的事件时,GitHub 会将 Webhook 事件发送到您的应用程序的 Webhook URL。

例如,您可以将您的 GitHub 应用程序订阅到拉取请求 Webhook 事件。当在应用程序已授予访问权限的存储库中打开拉取请求时,GitHub 会将拉取请求 Webhook 事件发送到您的应用程序的 Webhook URL。如果多个操作可以触发事件,则事件负载将包含一个 action 字段以指示触发事件的操作类型。在此示例中,action 的值为 opened,因为该事件是由打开拉取请求触发的。

如果您的应用程序正在监听这些 Webhook 事件的服务器上运行,那么您的应用程序在收到 Webhook 事件时可以采取操作。例如,您的应用程序可以在收到拉取请求 Webhook 事件时使用 GitHub API 向拉取请求发布评论。

有关更多信息,请参阅“将 Webhook 与 GitHub 应用程序一起使用”。有关可能的 Webhook 事件和操作的信息,请参阅“Webhook 事件和负载”。

先决条件

本教程要求您的计算机或 Codespace 运行 Node.js 版本 20 或更高版本以及 npm 版本 6.12.0 或更高版本。有关更多信息,请参阅 Node.js

本教程假设您对 JavaScript 和 ES6 语法有基本了解。

设置

以下部分将引导您完成以下组件的设置

  • 一个存储应用程序代码的存储库
  • 一种在本地接收 Webhook 的方法
  • 一个订阅“拉取请求”Webhook 事件、有权向拉取请求添加评论并使用您可以在本地接收的 Webhook URL 的 GitHub 应用程序注册

创建存储应用程序代码的存储库

  1. 创建一个存储应用程序代码的存储库。有关更多信息,请参阅“创建新的存储库”。
  2. 克隆上一步中的存储库。有关更多信息,请参阅“克隆存储库”。您可以使用本地克隆或 GitHub Codespaces。
  3. 在终端中,导航到存储克隆的目录。
  4. 如果目录中尚不存在 .gitignore 文件,请添加一个 .gitignore 文件。您稍后将向此文件添加内容。有关 .gitignore 文件的更多信息,请参阅“忽略文件”。

您将在后续步骤中向此存储库添加更多代码。

获取 Webhook 代理 URL

为了在本地开发应用程序,您可以使用 Webhook 代理 URL 将 Webhook 从 GitHub 转发到您的计算机或 Codespace。本教程使用 Smee.io 提供 Webhook 代理 URL 并转发 Webhook。

  1. 在浏览器中,导航到 https://smee.io/
  2. 点击开始新通道
  3. 复制“Webhook 代理 URL”下的完整 URL。您将在后面的步骤中使用此 URL。

注册 GitHub 应用程序

在本教程中,您必须拥有一个 GitHub 应用程序注册,该注册

  • 启用了 Webhook
  • 使用您可以本地接收的 Webhook URL
  • 拥有“拉取请求”存储库权限
  • 订阅“拉取请求”Webhook 事件

以下步骤将指导您完成使用这些设置注册 GitHub 应用程序。有关 GitHub 应用程序设置的更多信息,请参阅“注册 GitHub 应用程序”。

  1. 在 GitHub 上任何页面右上角,点击您的个人资料照片。

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

    • 对于个人帐户拥有的应用程序,点击设置
    • 对于组织拥有的应用程序
      1. 点击您的组织
      2. 在组织的右侧,点击设置
  3. 在左侧边栏中,点击 开发者设置

  4. 在左侧边栏中,点击GitHub 应用程序

  5. 点击新建 GitHub 应用程序

  6. 在“GitHub 应用程序名称”下,输入应用程序的名称。例如,USERNAME-webhook-test-app,其中 USERNAME 是您的 GitHub 用户名。

  7. 在“主页 URL”下,输入应用程序的 URL。例如,您可以使用您创建的存储应用程序代码的存储库的 URL。

  8. 在本教程中,跳过“识别和授权用户”和“安装后”部分。有关这些设置的更多信息,请参阅“注册 GitHub 应用程序”。

  9. 确保在“Webhook”下选择了活动

  10. 在“Webhook URL”下,输入您之前获得的 Webhook 代理 URL。有关更多信息,请参阅“获取 Webhook 代理 URL”。

  11. 在“Webhook 密钥”下,输入随机字符串。您将在后面使用此字符串。

  12. 在“存储库权限”下,“拉取请求”旁边,选择读取和写入

  13. 在“订阅事件”下,选择拉取请求

  14. 在“此 GitHub 应用程序可以在何处安装?”下,选择仅在此帐户上。如果您以后想发布您的应用程序,可以更改此设置。

  15. 点击创建 GitHub 应用程序

编写应用程序代码

以下部分将引导您完成编写代码以使您的应用程序响应 Webhook 事件。

安装依赖项

本教程使用 GitHub 的 octokit 模块来处理 Webhook 事件并发出 API 请求。有关 Octokit.js 的更多信息,请参阅“使用 REST API 和 JavaScript 编写脚本”和 Octokit.js 自述文件

本教程使用 dotenv 模块从 .env 文件中读取有关应用程序的信息。有关更多信息,请参阅 dotenv

本教程使用 Smee.io 将 Webhook 从 GitHub 转发到您的本地服务器。有关更多信息,请参阅 smee-client

  1. 在终端中,导航到存储克隆的目录。
  2. 运行 npm init --yes 以使用 npm 默认值创建 package.json 文件。
  3. 运行 npm install octokit
  4. 运行 npm install dotenv
  5. 运行 npm install smee-client --save-dev。由于您只会在开发应用程序时使用 Smee.io 转发 Webhook,因此这是一个开发依赖项。
  6. node_modules 添加到您的 .gitignore 文件中。

存储应用程序的识别信息和凭据

本教程将向您展示如何将应用程序的凭据和识别信息作为环境变量存储在 .env 文件中。部署应用程序时,您需要更改存储凭据的方式。有关更多信息,请参阅“部署您的应用程序”。

在执行这些步骤之前,请确保您使用的是安全的机器,因为您将本地存储您的凭据。

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

  2. 在该目录的顶层创建一个名为 .env 的文件。

  3. .env 添加到您的 .gitignore 文件中。这将防止您意外提交应用程序的凭据。

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

    文本
    APP_ID="YOUR_APP_ID"
    WEBHOOK_SECRET="YOUR_WEBHOOK_SECRET"
    PRIVATE_KEY_PATH="YOUR_PRIVATE_KEY_PATH"
    
  5. 导航到应用程序的设置页面

    1. 在 GitHub 上任何页面右上角,点击您的个人资料照片。

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

      • 对于个人帐户拥有的应用程序,点击设置
      • 对于组织拥有的应用程序
        1. 点击您的组织
        2. 在组织的右侧,点击设置
    3. 在左侧边栏中,点击 开发者设置

    4. 在左侧边栏中,点击GitHub 应用程序

    5. 在应用程序名称旁边,点击编辑

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

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

  8. 在应用程序的设置页面上,“私钥”下,点击生成私钥。您将看到一个 PEM 格式的私钥已下载到您的计算机上。有关更多信息,请参阅“管理 GitHub 应用程序的私钥”。

  9. 如果您使用的是 Codespace,请将下载的 PEM 文件移动到您的 Codespace 中,以便您的 Codespace 可以访问该文件。

  10. 在您的 .env 文件中,将 YOUR_PRIVATE_KEY_PATH 替换为您私钥的完整路径,包括 .pem 扩展名。

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

添加代码以响应 Webhook 事件

在存储克隆的目录的顶层,创建一个 JavaScript 文件以保存应用程序的代码。本教程将该文件命名为 app.js

将以下代码添加到 app.js 中。代码包含解释每个部分的注释。

JavaScript
import dotenv from "dotenv";
import {App} from "octokit";
import {createNodeMiddleware} from "@octokit/webhooks";
import fs from "fs";
import http from "http";

这些是此文件的依赖项。

您之前已安装了 dotenvoctokit 模块。@octokit/webhooksoctokit 模块的依赖项,因此您无需单独安装它。fshttp 依赖项是内置的 Node.js 模块。

dotenv.config();

这将读取您的 .env 文件并将该文件中的变量添加到 Node.js 中的 process.env 对象中。

const appId = process.env.APP_ID;
const webhookSecret = process.env.WEBHOOK_SECRET;
const privateKeyPath = process.env.PRIVATE_KEY_PATH;

这将环境变量的值分配给局部变量。

const privateKey = fs.readFileSync(privateKeyPath, "utf8");

这将读取私钥文件的内容。

const app = new App({
  appId: appId,
  privateKey: privateKey,
  webhooks: {
    secret: webhookSecret
  },
});

这将创建一个新的 Octokit 应用程序类的实例。

const messageForNewPRs = "Thanks for opening a new PR! Please follow our contributing guidelines to make your PR easier to review.";

这定义了应用程序将发布到拉取请求的消息。

async function handlePullRequestOpened({octokit, payload}) {
  console.log(`Received a pull request event for #${payload.pull_request.number}`);
  try {
    await octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
      owner: payload.repository.owner.login,
      repo: payload.repository.name,
      issue_number: payload.pull_request.number,
      body: messageForNewPRs,
      headers: {
        "x-github-api-version": "2022-11-28",
      },
    });
  } catch (error) {
    if (error.response) {
      console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`)
    }
    console.error(error)
  }
};

这添加了一个事件处理程序,您的代码将在稍后调用它。当调用此事件处理程序时,它会将事件记录到控制台。然后,它将使用 GitHub 的 REST API 向触发事件的拉取请求添加评论。

app.webhooks.on("pull_request.opened", handlePullRequestOpened);

这设置了一个 Webhook 事件侦听器。当您的应用程序收到 GitHub 发送的 Webhook 事件,其 X-GitHub-Event 标头值为 pull_requestaction 负载值为 opened 时,它会调用上面定义的 handlePullRequestOpened 事件处理程序。

app.webhooks.onError((error) => {
  if (error.name === "AggregateError") {
    console.error(`Error processing request: ${error.event}`);
  } else {
    console.error(error);
  }
});

这会记录发生的任何错误。

const port = 3000;
const host = 'localhost';
const path = "/api/webhook";
const localWebhookUrl = `http://${host}:${port}${path}`;

这确定服务器将在哪里侦听。

对于本地开发,您的服务器将在 localhost 上的端口 3000 上侦听。部署应用程序时,您将更改这些值。有关更多信息,请参阅“部署您的应用程序”。

const middleware = createNodeMiddleware(app.webhooks, {path});

这设置了一个中间件函数来处理传入的 Webhook 事件。

Octokit 的 createNodeMiddleware 函数负责为您生成此中间件函数。生成的中间件函数将

  • 检查传入 Webhook 事件的签名,以确保它与您的 Webhook 密钥匹配。这将验证传入的 Webhook 事件是否为有效的 GitHub 事件。
  • 解析 Webhook 事件有效负载并识别事件类型。
  • 触发相应的 Webhook 事件处理程序。
http.createServer(middleware).listen(port, () => {
  console.log(`Server is listening for events at: ${localWebhookUrl}`);
  console.log('Press Ctrl + C to quit.')
});

这会创建一个 Node.js 服务器,该服务器侦听传入的 HTTP 请求(包括来自 GitHub 的 Webhook 有效负载)在指定端口上。当服务器收到请求时,它会执行您之前定义的 middleware 函数。服务器运行后,它会将消息记录到控制台以指示它正在侦听。

// These are the dependencies for this file.
//
// You installed the `dotenv` and `octokit` modules earlier. The `@octokit/webhooks` is a dependency of the `octokit` module, so you don't need to install it separately. The `fs` and `http` dependencies are built-in Node.js modules.
import dotenv from "dotenv";
import {App} from "octokit";
import {createNodeMiddleware} from "@octokit/webhooks";
import fs from "fs";
import http from "http";

// This reads your `.env` file and adds the variables from that file to the `process.env` object in Node.js.
dotenv.config();

// This assigns the values of your environment variables to local variables.
const appId = process.env.APP_ID;
const webhookSecret = process.env.WEBHOOK_SECRET;
const privateKeyPath = process.env.PRIVATE_KEY_PATH;

// This reads the contents of your private key file.
const privateKey = fs.readFileSync(privateKeyPath, "utf8");

// This creates a new instance of the Octokit App class.
const app = new App({
  appId: appId,
  privateKey: privateKey,
  webhooks: {
    secret: webhookSecret
  },
});

// This defines the message that your app will post to pull requests.
const messageForNewPRs = "Thanks for opening a new PR! Please follow our contributing guidelines to make your PR easier to review.";

// This adds an event handler that your code will call later. When this event handler is called, it will log the event to the console. Then, it will use GitHub's REST API to add a comment to the pull request that triggered the event.
async function handlePullRequestOpened({octokit, payload}) {
  console.log(`Received a pull request event for #${payload.pull_request.number}`);

  try {
    await octokit.request("POST /repos/{owner}/{repo}/issues/{issue_number}/comments", {
      owner: payload.repository.owner.login,
      repo: payload.repository.name,
      issue_number: payload.pull_request.number,
      body: messageForNewPRs,
      headers: {
        "x-github-api-version": "2022-11-28",
      },
    });
  } catch (error) {
    if (error.response) {
      console.error(`Error! Status: ${error.response.status}. Message: ${error.response.data.message}`)
    }
    console.error(error)
  }
};

// This sets up a webhook event listener. When your app receives a webhook event from GitHub with a `X-GitHub-Event` header value of `pull_request` and an `action` payload value of `opened`, it calls the `handlePullRequestOpened` event handler that is defined above.
app.webhooks.on("pull_request.opened", handlePullRequestOpened);

// This logs any errors that occur.
app.webhooks.onError((error) => {
  if (error.name === "AggregateError") {
    console.error(`Error processing request: ${error.event}`);
  } else {
    console.error(error);
  }
});

// This determines where your server will listen.
//
// For local development, your server will listen to port 3000 on `localhost`. When you deploy your app, you will change these values. For more information, see "[Deploy your app](#deploy-your-app)."
const port = 3000;
const host = 'localhost';
const path = "/api/webhook";
const localWebhookUrl = `http://${host}:${port}${path}`;

// This sets up a middleware function to handle incoming webhook events.
//
// Octokit's `createNodeMiddleware` function takes care of generating this middleware function for you. The resulting middleware function will:
//
//    - Check the signature of the incoming webhook event to make sure that it matches your webhook secret. This verifies that the incoming webhook event is a valid GitHub event.
//    - Parse the webhook event payload and identify the type of event.
//    - Trigger the corresponding webhook event handler.
const middleware = createNodeMiddleware(app.webhooks, {path});

// This creates a Node.js server that listens for incoming HTTP requests (including webhook payloads from GitHub) on the specified port. When the server receives a request, it executes the `middleware` function that you defined earlier. Once the server is running, it logs messages to the console to indicate that it is listening.
http.createServer(middleware).listen(port, () => {
  console.log(`Server is listening for events at: ${localWebhookUrl}`);
  console.log('Press Ctrl + C to quit.')
});

添加一个脚本以运行应用程序的代码

  1. package.json 文件的 scripts 对象中,添加一个名为 server 的脚本,该脚本运行 node app.js。例如

    JSON
    "scripts": {
      "server": "node app.js"
    }
    

    如果您将包含应用程序代码的文件命名为 app.js 以外的其他名称,请将 app.js 替换为包含应用程序代码的文件的相对路径。

  2. 在您的 package.json 文件中,添加一个顶级键 type,其值为 module。例如

       {
        // rest of the JSON object,
        "version": "1.0.0",
        "description": "",
        "type": "module",
        // rest of the JSON object,
      }
    

您的 package.json 文件应如下所示。name 值以及 dependenciesdevDependencies 下的版本号可能会有所不同。

  {
  "name": "github-app-webhook-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "server": "node app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.0.3",
    "octokit": "^2.0.14"
  },
  "devDependencies": {
    "smee-client": "^1.2.3"
  }
}

测试

请按照以下步骤测试您上面创建的应用程序。

安装您的应用程序

为了使您的应用程序能够在存储库的拉取请求中留下注释,它必须安装在拥有该存储库的帐户上并授予对该存储库的访问权限。由于您的应用程序是私有的,因此它只能安装在拥有该应用程序的帐户上。

  1. 在拥有您创建的应用程序的帐户中,创建一个新的存储库以安装该应用程序。有关更多信息,请参阅“创建新的存储库”。

  2. 导航到应用程序的设置页面

    1. 在 GitHub 上任何页面右上角,点击您的个人资料照片。

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

      • 对于个人帐户拥有的应用程序,点击设置
      • 对于组织拥有的应用程序
        1. 点击您的组织
        2. 在组织的右侧,点击设置
    3. 在左侧边栏中,点击 开发者设置

    4. 在左侧边栏中,点击GitHub 应用程序

    5. 在应用程序名称旁边,点击编辑

  3. 点击**公共页面**。

  4. 点击**安装**。

  5. 选择**仅选择存储库**。

  6. 选择**选择存储库**下拉菜单,然后点击您在本节开头选择的存储库。

  7. 点击**安装**。

启动您的服务器

为了进行测试,您将使用您的计算机或 Codespace 作为服务器。只有在您的服务器正在运行时,您的应用程序才会响应 Webhook。

  1. 在终端中,导航到存储应用程序代码的目录。

  2. 要接收来自 Smee.io 的转发 Webhook,请运行 npx smee -u WEBHOOK_PROXY_URL -t https://127.0.0.1:3000/api/webhook。将 WEBHOOK_PROXY_URL 替换为您之前获得的 Webhook 代理 URL。如果您忘记了 URL,您可以在应用程序设置页面上的“Webhook URL”字段中找到它。

    您应该会看到如下所示的输出,其中 WEBHOOK_PROXY_URL 是您的 Webhook 代理 URL

    Forwarding WEBHOOK_PROXY_URL to https://127.0.0.1:3000/api/webhook
    Connected WEBHOOK_PROXY_URL
    
  3. 在第二个终端窗口中,导航到存储应用程序代码的目录。

  4. 运行 npm run server。您的终端应显示 Server is listening for events at: https://127.0.0.1:3000/api/webhook

测试您的应用程序

现在您的服务器正在运行并接收转发 Webhook 事件,您可以通过在安装应用程序时选择的存储库上打开拉取请求来测试您的应用程序。

  1. 在您安装应用程序时选择的存储库上打开拉取请求。有关更多信息,请参阅“创建拉取请求”。

    请确保使用您在安装应用程序时选择的存储库,而不是存储应用程序代码的存储库。有关更多信息,请参阅“安装您的应用程序”。

  2. 导航到 smee.io 上的 Webhook 代理 URL。您应该会看到一个 pull_request 事件。这表示 GitHub 在您创建拉取请求时成功发送了拉取请求事件。

  3. 在您运行 npm run server 的终端中,您应该会看到类似“Received a pull request event for #1”的内容,其中 # 后面的整数是您打开的拉取请求的编号。

  4. 在拉取请求的时间线上,您应该会看到来自您的应用程序的注释。

  5. 在两个终端窗口中,都输入 Ctrl+C 以停止您的服务器并停止侦听转发 Webhook。

后续步骤

现在您拥有了一个响应 Webhook 事件的应用程序,您可能希望扩展应用程序的代码、部署应用程序并公开您的应用程序。

修改应用程序代码

本教程演示了如何在打开拉取请求时在拉取请求上发布注释。您可以更新代码以响应不同类型的 Webhook 事件,或在响应 Webhook 事件时执行其他操作。

请记住,如果您的应用程序需要您想要进行的 API 请求或您想要接收的 Webhook 事件的其他权限,请更新应用程序的权限。有关更多信息,请参阅“为 GitHub 应用程序选择权限”。

本教程将所有代码存储在一个文件中,但您可能希望将函数和组件移动到单独的文件中。

部署您的应用程序

本教程演示了如何在本地开发应用程序。当您准备好部署应用程序时,您需要进行更改以服务您的应用程序并保持应用程序的凭据安全。您采取的步骤取决于您使用的服务器,但以下部分提供了一般指南。

在服务器上托管您的应用程序

本教程使用您的计算机或 Codespace 作为服务器。一旦应用程序准备就绪以供生产使用,您应该将应用程序部署到专用服务器。例如,您可以使用Azure 应用服务

更新 Webhook URL

一旦您拥有一个设置为接收来自 GitHub 的 Webhook 流量的服务器,请更新应用程序设置中的 Webhook URL。您不应在生产环境中使用 Smee.io 转发 Webhook。

更新 porthost 常量

当您部署应用程序时,您将希望更改服务器侦听的主机和端口。

例如,您可以在服务器上设置一个 PORT 环境变量以指示服务器应侦听的端口。您可以在服务器上设置一个 NODE_ENV 环境变量为 production。然后,您可以更新代码定义 porthost 常量的位置,以便您的服务器侦听所有可用的网络接口 (0.0.0.0) 而不是您部署端口上的本地网络接口 (localhost)

JavaScript
const port = process.env.PORT || 3000;
const host = process.env.NODE_ENV === 'production' ? '0.0.0.0' : 'localhost';

保护应用程序的凭据

您永远不应公开应用程序的私钥或 Webhook 密钥。本教程将应用程序的凭据存储在 gitignored 的 .env 文件中。当您部署应用程序时,您应该选择一种安全的方式来存储凭据并更新您的代码以相应地获取值。例如,您可以使用像Azure Key Vault这样的密钥管理服务来存储凭据。当您的应用程序运行时,它可以检索凭据并将它们存储在部署应用程序的服务器上的环境变量中。

有关更多信息,请参阅“创建 GitHub 应用程序的最佳实践”。

共享您的应用程序

如果您想与其他用户和组织共享您的应用程序,请公开您的应用程序。有关更多信息,请参阅“使 GitHub 应用程序公开或私有”。

遵循最佳实践

您应该努力遵循 GitHub 应用程序的最佳实践。有关更多信息,请参阅“创建 GitHub 应用程序的最佳实践”。