跳至主要内容

自动重新投递 GitHub 应用 Webhook 的失败投递

您可以编写脚本来处理 GitHub App Webhook 的失败投递。

关于自动重新投递失败的投递

本文介绍如何编写脚本来查找并重新投递 GitHub App Webhook 的失败投递。有关失败投递的更多信息,请参阅 处理失败的 Webhook 投递

本示例展示了以下内容

  • 一个用于查找并重新投递 GitHub App Webhook 失败投递的脚本
  • 脚本需要哪些凭证,以及如何将凭证安全地存储为 GitHub Actions 密钥
  • 一个可以安全访问凭证并定期运行脚本的 GitHub Actions 工作流

本示例使用 GitHub Actions,但您也可以在处理 Webhook 投递的服务器上运行此脚本。更多信息请参阅替代方法

为脚本存储凭证

查找并重新投递失败 Webhook 的端点需要 JSON Web 令牌,该令牌由您的应用 ID 和私钥生成。

获取和更新环境变量值的端点需要个人访问令牌、GitHub App 安装访问令牌或 GitHub App 用户访问令牌。此示例使用个人访问令牌。如果您的 GitHub App 已安装在运行此工作流的仓库中且具备写入仓库变量的权限,您可以修改此示例,在 GitHub Actions 工作流中创建安装访问令牌,而不是使用个人访问令牌。更多信息,请参阅 在 GitHub Actions 工作流中使用 GitHub App 进行身份验证的 API 请求

  1. 查找您的 GitHub App 的应用 ID。您可以在应用的设置页面上找到应用 ID。应用 ID 与客户端 ID 不同。有关如何导航到 GitHub App 设置页面的更多信息,请参阅 修改 GitHub App 注册信息
  2. 将上一步的应用 ID 作为 GitHub Actions secret 存储在您希望运行工作流的仓库中。有关存储 secret 的更多信息,请参阅 在 GitHub Actions 中使用 secret
  3. 为您的应用生成私钥。有关生成私钥的更多信息,请参阅 管理 GitHub App 私钥
  4. 将上一步的私钥(包括 -----BEGIN RSA PRIVATE KEY----------END RSA PRIVATE KEY-----)作为 GitHub Actions secret 存储在您希望运行工作流的仓库中。
  5. 创建具有以下访问权限的个人访问令牌。更多信息请参阅管理个人访问令牌
    • 对于细粒度个人访问令牌,请授予该令牌
      • 对仓库变量的写权限
      • 对运行此工作流的仓库的访问权限
    • 对于传统个人访问令牌,请授予该令牌 repo 范围。
  6. 将上一步的个人访问令牌作为 GitHub Actions secret 存储在您希望运行工作流的仓库中。

添加将运行脚本的工作流

本节展示如何使用 GitHub Actions 工作流安全访问前一节中存储的凭证、设置环境变量,并定期运行脚本以查找并重新投递失败的投递。

将以下 GitHub Actions 工作流复制到您希望工作流运行的仓库的.github/workflows目录下的 YAML 文件中。按照下文所述替换 Run script 步骤中的占位符。

YAML
name: Redeliver failed webhook deliveries
on:
  schedule:
    - cron: '40 */6 * * *'
  workflow_dispatch:

此工作流每 6 小时运行一次,或在手动触发时运行。

permissions:
  contents: read

此工作流将使用内置的GITHUB_TOKEN检出仓库内容。该令牌拥有相应的权限。

jobs:
  redeliver-failed-deliveries:
    name: Redeliver failed deliveries
    runs-on: ubuntu-latest
    steps:
      - name: Check out repo content
        uses: actions/checkout@v5

此工作流将运行存储在仓库中的脚本。此步骤检出仓库内容,以便工作流能够访问脚本。

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

此步骤设置 Node.js。该工作流将运行的脚本使用 Node.js。

      - name: Install dependencies
        run: npm install octokit

此步骤安装 octokit 库。该工作流将运行的脚本使用 octokit 库。

      - name: Run script
        env:
          APP_ID: ${{ secrets.YOUR_APP_ID_SECRET_NAME }}
          PRIVATE_KEY: ${{ secrets.YOUR_PRIVATE_KEY_SECRET_NAME }}
          TOKEN: ${{ secrets.YOUR_TOKEN_SECRET_NAME }}
          LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'
          WORKFLOW_REPO: ${{ github.event.repository.name }}
          WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}
        run: |
          node .github/workflows/scripts/redeliver-failed-deliveries.mjs

此步骤设置一些环境变量,然后运行脚本以查找并重新投递失败的 Webhook 投递。

  • YOUR_APP_ID_SECRET_NAME 替换为您存储应用 ID 的 secret 名称。
  • YOUR_PRIVATE_KEY_SECRET_NAME 替换为您存储私钥的 secret 名称。
  • YOUR_TOKEN_SECRET_NAME 替换为您存储个人访问令牌的 secret 名称。
  • YOUR_LAST_REDELIVERY_VARIABLE_NAME替换为您希望在存放此工作流的仓库中使用的配置变量名称。名称只能包含字母、数字和_,且不能以GITHUB_或数字开头。更多信息请参阅在变量中存储信息
#
name: Redeliver failed webhook deliveries

# This workflow runs every 6 hours or when manually triggered.
on:
  schedule:
    - cron: '40 */6 * * *'
  workflow_dispatch:

# This workflow will use the built in `GITHUB_TOKEN` to check out the repository contents. This grants `GITHUB_TOKEN` permission to do that.
permissions:
  contents: read

#
jobs:
  redeliver-failed-deliveries:
    name: Redeliver failed deliveries
    runs-on: ubuntu-latest
    steps:
      # This workflow will run a script that is stored in the repository. This step checks out the repository contents so that the workflow can access the script.
      - name: Check out repo content
        uses: actions/checkout@v5

      # This step sets up Node.js. The script that this workflow will run uses Node.js.
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      # This step installs the octokit library. The script that this workflow will run uses the octokit library.
      - name: Install dependencies
        run: npm install octokit

      # This step sets some environment variables, then runs a script to find and redeliver failed webhook deliveries.
      # - Replace `YOUR_APP_ID_SECRET_NAME` with the name of the secret where you stored your app ID.
      # - Replace `YOUR_PRIVATE_KEY_SECRET_NAME` with the name of the secret where you stored your private key.
      # - Replace `YOUR_TOKEN_SECRET_NAME` with the name of the secret where you stored your personal access token.
      # - Replace `YOUR_LAST_REDELIVERY_VARIABLE_NAME` with the name that you want to use for a configuration variable that will be stored in the repository where this workflow is stored. The name can be any string that contains only alphanumeric characters and `_`, and does not start with `GITHUB_` or a number. For more information, see [AUTOTITLE](/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows).
      
      - name: Run script
        env:
          APP_ID: ${{ secrets.YOUR_APP_ID_SECRET_NAME }}
          PRIVATE_KEY: ${{ secrets.YOUR_PRIVATE_KEY_SECRET_NAME }}
          TOKEN: ${{ secrets.YOUR_TOKEN_SECRET_NAME }}
          LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'
          
          WORKFLOW_REPO: ${{ github.event.repository.name }}
          WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}
        run: |
          node .github/workflows/scripts/redeliver-failed-deliveries.mjs

添加脚本

本节演示如何编写脚本以查找并重新投递失败的投递。

将此脚本复制到名为 .github/workflows/scripts/redeliver-failed-deliveries.mjs 的文件中,放在您上述保存 GitHub Actions 工作流文件的同一仓库中。

JavaScript
import { App, Octokit } from "octokit";

此脚本使用 GitHub 的 Octokit SDK 发起 API 请求。更多信息请参阅AUTOTITLE

async function checkAndRedeliverWebhooks() {
  const APP_ID = process.env.APP_ID;
  const PRIVATE_KEY = process.env.PRIVATE_KEY;
  const TOKEN = process.env.TOKEN;
  const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;
  const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO;
  const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;

获取由 GitHub Actions 工作流设置的环境变量的值。

  const app = new App({
    appId: APP_ID,
    privateKey: PRIVATE_KEY,
  });

使用在 GitHub Actions 工作流中设置的应用 ID 和私钥值,创建 octokit App 实例。

这将用于向与 webhook 相关的端点发起 API 请求。

  const octokit = new Octokit({ 
    auth: TOKEN,
  });
  try {

使用工作流中设置的令牌值创建 Octokit 实例。

这将用于更新存储该脚本上次运行时间的配置变量。

    const lastStoredRedeliveryTime = await getVariable({
      variableName: LAST_REDELIVERY_VARIABLE_NAME,
      repoOwner: WORKFLOW_REPO_OWNER,
      repoName: WORKFLOW_REPO_NAME,
      octokit,
    });
    const lastWebhookRedeliveryTime = lastStoredRedeliveryTime || (Date.now() - (24 * 60 * 60 * 1000)).toString();

从配置变量中获取上次脚本运行的时间。如果变量未定义,则使用当前时间减去 24 小时。

    const newWebhookRedeliveryTime = Date.now().toString();

记录本次脚本开始重新投递 Webhook 的时间。

    const deliveries = await fetchWebhookDeliveriesSince({lastWebhookRedeliveryTime, app});

获取在 lastWebhookRedeliveryTime 之后投递的 Webhook 投递记录。

    let deliveriesByGuid = {};
    for (const delivery of deliveries) {
      deliveriesByGuid[delivery.guid]
        ? deliveriesByGuid[delivery.guid].push(delivery)
        : (deliveriesByGuid[delivery.guid] = [delivery]);
    }

合并具有相同全局唯一标识符(GUID)的投递。相同投递的重新投递会拥有相同的 GUID。

    let failedDeliveryIDs = [];
    for (const guid in deliveriesByGuid) {
      const deliveries = deliveriesByGuid[guid];
      const anySucceeded = deliveries.some(
        (delivery) => delivery.status === "OK"
      );
      if (!anySucceeded) {
        failedDeliveryIDs.push(deliveries[0].id);
      }
    }

对于每个 GUID,如果在该时间段内没有成功投递,则获取该 GUID 对应的其中一次投递的 ID。

这可防止在同一次投递多次失败时出现重复的重新投递,也可防止已经成功重新投递的投递再次被重新投递。

    for (const deliveryId of failedDeliveryIDs) {
      await redeliverWebhook({deliveryId, app});
    }

重新投递所有失败的投递。

    await updateVariable({
      variableName: LAST_REDELIVERY_VARIABLE_NAME,
      value: newWebhookRedeliveryTime,
      variableExists: Boolean(lastStoredRedeliveryTime),
      repoOwner: WORKFLOW_REPO_OWNER,
      repoName: WORKFLOW_REPO_NAME,
      octokit,
      });

更新配置变量(如果变量不存在则创建),以存储本次脚本开始的时间。该值将在下次脚本运行时使用。

    console.log(
      `Redelivered ${
        failedDeliveryIDs.length
      } failed webhook deliveries out of ${
        deliveries.length
      } total deliveries since ${Date(lastWebhookRedeliveryTime)}.`
    );
  } catch (error) {

记录重新投递的数量。

    if (error.response) {
      console.error(
        `Failed to check and redeliver webhooks: ${error.response.data.message}`
      );
    }
    console.error(error);
    throw(error);
  }
}

如果出现错误,先将错误记录到工作流运行日志中,然后抛出错误,使工作流运行标记为失败。

async function fetchWebhookDeliveriesSince({lastWebhookRedeliveryTime, app}) {
  const iterator = app.octokit.paginate.iterator(
    "GET /app/hook/deliveries",
    {
      per_page: 100,
      headers: {
        "x-github-api-version": "2026-03-10",
      },
    }
  );
  const deliveries = [];
  for await (const { data } of iterator) {
    const oldestDeliveryTimestamp = new Date(
      data[data.length - 1].delivered_at
    ).getTime();
    if (oldestDeliveryTimestamp < lastWebhookRedeliveryTime) {
      for (const delivery of data) {
        if (
          new Date(delivery.delivered_at).getTime() > lastWebhookRedeliveryTime
        ) {
          deliveries.push(delivery);
        } else {
          break;
        }
      }
      break;
    } else {
      deliveries.push(...data);
    }
  }
  return deliveries;
}

此函数将获取自 lastWebhookRedeliveryTime 以来的所有 Webhook 投递。它使用 octokit.paginate.iterator() 方法遍历分页结果。更多信息请参阅AUTOTITLE

如果某一页结果中包含在 lastWebhookRedeliveryTime 之前的投递,则只保留在 lastWebhookRedeliveryTime 之后的投递并停止;否则,保留该页的所有投递并请求下一页。

async function redeliverWebhook({deliveryId, app}) {
  await app.octokit.request("POST /app/hook/deliveries/{delivery_id}/attempts", {
    delivery_id: deliveryId,
  });
}

此函数将重新投递一次失败的 Webhook 投递。

async function getVariable({ variableName, repoOwner, repoName, octokit }) {
  try {
    const {
      data: { value },
    } = await octokit.request(
      "GET /repos/{owner}/{repo}/actions/variables/{name}",
      {
        owner: repoOwner,
        repo: repoName,
        name: variableName,
      }
    );
    return value;
  } catch (error) {
    if (error.status === 404) {
      return undefined;
    } else {
      throw error;
    }
  }
}

此函数获取配置变量的值。如果变量不存在,端点会返回 404 响应,函数返回 undefined

async function updateVariable({
  variableName,
  value,
  variableExists,
  repoOwner,
  repoName,
  octokit,
}) {
  if (variableExists) {
    await octokit.request(
      "PATCH /repos/{owner}/{repo}/actions/variables/{name}",
      {
        owner: repoOwner,
        repo: repoName,
        name: variableName,
        value: value,
      }
    );
  } else {
    await octokit.request("POST /repos/{owner}/{repo}/actions/variables", {
      owner: repoOwner,
      repo: repoName,
      name: variableName,
      value: value,
    });
  }
}

此函数将更新配置变量(如果变量不存在则创建)。更多信息请参阅在变量中存储信息

(async () => {
  await checkAndRedeliverWebhooks();
})();

这将执行 checkAndRedeliverWebhooks 函数。

// This script uses GitHub's Octokit SDK to make API requests. For more information, see [AUTOTITLE](/rest/guides/scripting-with-the-rest-api-and-javascript).
import { App, Octokit } from "octokit";

//
async function checkAndRedeliverWebhooks() {
  // Get the values of environment variables that were set by the GitHub Actions workflow.
  const APP_ID = process.env.APP_ID;
  const PRIVATE_KEY = process.env.PRIVATE_KEY;
  const TOKEN = process.env.TOKEN;
  const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;
  
  const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO;
  const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;

  // Create an instance of the octokit `App` using the app ID and private key values that were set in the GitHub Actions workflow.
  //
  // This will be used to make API requests to the webhook-related endpoints.
  const app = new App({
    appId: APP_ID,
    privateKey: PRIVATE_KEY,
  });

  // Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow.
  //
  // This will be used to update the configuration variable that stores the last time that this script ran.
  const octokit = new Octokit({ 
    auth: TOKEN,
  });

  try {
    // Get the last time that this script ran from the configuration variable. If the variable is not defined, use the current time minus 24 hours.
    const lastStoredRedeliveryTime = await getVariable({
      variableName: LAST_REDELIVERY_VARIABLE_NAME,
      repoOwner: WORKFLOW_REPO_OWNER,
      repoName: WORKFLOW_REPO_NAME,
      octokit,
    });
    const lastWebhookRedeliveryTime = lastStoredRedeliveryTime || (Date.now() - (24 * 60 * 60 * 1000)).toString();

    // Record the time that this script started redelivering webhooks.
    const newWebhookRedeliveryTime = Date.now().toString();

    // Get the webhook deliveries that were delivered after `lastWebhookRedeliveryTime`.
    const deliveries = await fetchWebhookDeliveriesSince({lastWebhookRedeliveryTime, app});

    // Consolidate deliveries that have the same globally unique identifier (GUID). The GUID is constant across redeliveries of the same delivery.
    let deliveriesByGuid = {};
    for (const delivery of deliveries) {
      deliveriesByGuid[delivery.guid]
        ? deliveriesByGuid[delivery.guid].push(delivery)
        : (deliveriesByGuid[delivery.guid] = [delivery]);
    }

    // For each GUID value, if no deliveries for that GUID have been successfully delivered within the time frame, get the delivery ID of one of the deliveries with that GUID.
    //
    // This will prevent duplicate redeliveries if a delivery has failed multiple times.
    // This will also prevent redelivery of failed deliveries that have already been successfully redelivered.
    let failedDeliveryIDs = [];
    for (const guid in deliveriesByGuid) {
      const deliveries = deliveriesByGuid[guid];
      const anySucceeded = deliveries.some(
        (delivery) => delivery.status === "OK"
      );
      if (!anySucceeded) {
        failedDeliveryIDs.push(deliveries[0].id);
      }
    }

    // Redeliver any failed deliveries.
    for (const deliveryId of failedDeliveryIDs) {
      await redeliverWebhook({deliveryId, app});
    }

    // Update the configuration variable (or create the variable if it doesn't already exist) to store the time that this script started.
    // This value will be used next time this script runs.
    await updateVariable({
      variableName: LAST_REDELIVERY_VARIABLE_NAME,
      value: newWebhookRedeliveryTime,
      variableExists: Boolean(lastStoredRedeliveryTime),
      repoOwner: WORKFLOW_REPO_OWNER,
      repoName: WORKFLOW_REPO_NAME,
      octokit,
      });

    // Log the number of redeliveries.
    console.log(
      `Redelivered ${
        failedDeliveryIDs.length
      } failed webhook deliveries out of ${
        deliveries.length
      } total deliveries since ${Date(lastWebhookRedeliveryTime)}.`
    );
  } catch (error) {
    // If there was an error, log the error so that it appears in the workflow run log, then throw the error so that the workflow run registers as a failure.
    if (error.response) {
      console.error(
        `Failed to check and redeliver webhooks: ${error.response.data.message}`
      );
    }
    console.error(error);
    throw(error);
  }
}

// This function will fetch all of the webhook deliveries that were delivered since `lastWebhookRedeliveryTime`.
// It uses the `octokit.paginate.iterator()` method to iterate through paginated results. For more information, see [AUTOTITLE](/rest/guides/scripting-with-the-rest-api-and-javascript#making-paginated-requests).
//
// If a page of results includes deliveries that occurred before `lastWebhookRedeliveryTime`,
// it will store only the deliveries that occurred after `lastWebhookRedeliveryTime` and then stop.
// Otherwise, it will store all of the deliveries from the page and request the next page.
async function fetchWebhookDeliveriesSince({lastWebhookRedeliveryTime, app}) {
  const iterator = app.octokit.paginate.iterator(
    "GET /app/hook/deliveries",
    {
      per_page: 100,
      headers: {
        "x-github-api-version": "2026-03-10",
      },
    }
  );

  const deliveries = [];

  for await (const { data } of iterator) {
    const oldestDeliveryTimestamp = new Date(
      data[data.length - 1].delivered_at
    ).getTime();

    if (oldestDeliveryTimestamp < lastWebhookRedeliveryTime) {
      for (const delivery of data) {
        if (
          new Date(delivery.delivered_at).getTime() > lastWebhookRedeliveryTime
        ) {
          deliveries.push(delivery);
        } else {
          break;
        }
      }
      break;
    } else {
      deliveries.push(...data);
    }
  }

  return deliveries;
}

// This function will redeliver a failed webhook delivery.
async function redeliverWebhook({deliveryId, app}) {
  await app.octokit.request("POST /app/hook/deliveries/{delivery_id}/attempts", {
    delivery_id: deliveryId,
  });
}

// This function gets the value of a configuration variable.
// If the variable does not exist, the endpoint returns a 404 response and this function returns `undefined`.
async function getVariable({ variableName, repoOwner, repoName, octokit }) {
  try {
    const {
      data: { value },
    } = await octokit.request(
      "GET /repos/{owner}/{repo}/actions/variables/{name}",
      {
        owner: repoOwner,
        repo: repoName,
        name: variableName,
      }
    );
    return value;
  } catch (error) {
    if (error.status === 404) {
      return undefined;
    } else {
      throw error;
    }
  }
}

// This function will update a configuration variable (or create the variable if it doesn't already exist). For more information, see [AUTOTITLE](/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows).
async function updateVariable({
  variableName,
  value,
  variableExists,
  repoOwner,
  repoName,
  octokit,
}) {
  if (variableExists) {
    await octokit.request(
      "PATCH /repos/{owner}/{repo}/actions/variables/{name}",
      {
        owner: repoOwner,
        repo: repoName,
        name: variableName,
        value: value,
      }
    );
  } else {
    await octokit.request("POST /repos/{owner}/{repo}/actions/variables", {
      owner: repoOwner,
      repo: repoName,
      name: variableName,
      value: value,
    });
  }
}

// This will execute the `checkAndRedeliverWebhooks` function.
(async () => {
  await checkAndRedeliverWebhooks();
})();

测试脚本

您可以手动触发工作流来测试脚本。更多信息请参阅手动运行工作流以及使用工作流运行日志

替代方法

此示例使用 GitHub Actions 安全地存储凭证并按计划运行脚本。不过,如果您希望在自己处理 webhook 投递的服务器上运行此脚本,也可以

  • 以其他安全方式存储凭证,例如使用像Azure Key Vault这样的密钥管理器。您还需要修改脚本以从新位置读取凭证。
  • 在服务器上按计划运行脚本,例如使用 cron 作业或任务计划程序。
  • 将脚本修改为将上次运行时间存储在服务器可访问和更新的位置。如果您决定不将上次运行时间存储为 GitHub Actions secret,则无需使用个人访问令牌,并且可以去掉访问和更新配置变量的 API 调用。
© . This site is unofficial and not affiliated with GitHub, Inc.