跳至主要内容

构建响应 Webhook 事件的 GitHub 应用

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

简介

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

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

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

关于 Webhook

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

例如,您可以将您的 GitHub 应用订阅到 pull request Webhook 事件。当在您的应用被授予访问权限的仓库中打开 pull request 时,GitHub 会将 pull request Webhook 事件发送到您的应用的 Webhook URL。如果多个操作可以触发事件,事件负载将包含一个 action 字段,以指示触发事件的操作类型。在本例中,action 的值为 opened,因为事件是由打开 pull request 触发的。

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

有关更多信息,请参阅 "使用 GitHub 应用的 Webhook"。有关可能的 Webhook 事件和操作的信息,请参阅 "Webhook 事件和负载"。

先决条件

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

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

设置

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

  • 一个存储应用代码的仓库
  • 一种在本地接收 Webhook 的方法
  • 一个订阅了“pull request”Webhook 事件的 GitHub 应用注册,具有向 pull request 添加评论的权限,并使用您可以在本地接收的 Webhook URL

创建一个存储应用代码的仓库

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

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

获取 Webhook 代理 URL

为了在本地开发您的应用程序,您可以使用 Webhook 代理 URL 将来自 GitHub 的 Webhook 转发到您的计算机或 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 secret”下,输入一个随机字符串。您将在稍后使用此字符串。
  12. 在“Repository permissions”下,在“Pull requests”旁边,选择**Read & write**。
  13. 在“Subscribe to events”下,选择**Pull request**。
  14. 在“Where can this GitHub App be installed?”下,选择**Only on this account**。如果您想发布您的应用程序,您可以稍后更改此设置。
  15. 点击**Create GitHub App**。

编写您的应用程序代码

以下部分将引导您编写代码,使您的应用程序能够响应 webhook 事件。

安装依赖项

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

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

本教程使用 Smee.io 将来自 GitHub 的 webhook 转发到您的本地服务器。有关更多信息,请参阅 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 App 类的实例。

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 接收带有 X-GitHub-Event 标头值为 pull_requestaction 有效负载值为 opened 的 Webhook 事件时,它将调用上面定义的 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 的终端中,您应该看到类似“收到拉取请求事件 #1”的内容,其中 # 后面的整数是您打开的拉取请求的编号。

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

  5. 在两个终端窗口中,输入 Ctrl+C 以停止服务器并停止监听转发 webhook。

下一步

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

修改应用程序代码

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

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

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

部署您的应用程序

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

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

本教程使用您的计算机或代码空间作为服务器。应用程序准备好投入生产使用后,您应该将应用程序部署到专用服务器。例如,您可以使用 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 密钥。本教程将您的应用程序凭据存储在一个被 gitignore 的 .env 文件中。当您部署应用程序时,您应该选择一种安全的方式来存储凭据,并更新您的代码以相应地获取值。例如,您可以使用像 Azure 密钥保管库 这样的秘密管理服务来存储凭据。当您的应用程序运行时,它可以检索凭据并将它们存储在应用程序部署的服务器上的环境变量中。

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

分享您的应用程序

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

遵循最佳实践

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