{"meta":{"title":"自动重新传送存储库 Webhook 的失败交付","intro":"可以编写脚本来处理存储库 Webhook 的失败交付。","product":"Webhook","breadcrumbs":[{"href":"/zh/webhooks","title":"Webhook"},{"href":"/zh/webhooks/using-webhooks","title":"使用网络钩子（Webhook）"},{"href":"/zh/webhooks/using-webhooks/automatically-redelivering-failed-deliveries-for-a-repository-webhook","title":"自动重新发送到代码库"}],"documentType":"article"},"body":"# 自动重新传送存储库 Webhook 的失败交付\n\n可以编写脚本来处理存储库 Webhook 的失败交付。\n\n## 关于自动重新传送失败交付\n\n本文介绍如何编写脚本来查找和重新传送存储库 Webhook 的失败交付。 有关失败的交付的详细信息，请参阅“[处理失败的 Webhook 交付](/zh/webhooks/using-webhooks/handling-failed-webhook-deliveries)”。\n\n这个示例向您展示了：\n\n* 用于查找并重新传送存储库 Webhook 的失败交付的脚本\n* 脚本需要哪些凭据，以及如何将凭据安全地存储为 GitHub Actions 机密\n* 一个可以安全地访问凭据并定期运行脚本的 GitHub Actions 工作流\n\n此示例使用 GitHub Actions，但也可以在处理 Webhook 传送的服务器上运行此脚本。 有关详细信息，请参阅[替代方法](#alternative-methods)。\n\n## 为脚本存储凭证\n\n内置 `GITHUB_TOKEN` 功能没有足够的权限重新交付 Webhook。 此示例用`GITHUB_TOKEN`代替使用personal access token。 或者，你也可以不创建 personal access token，而是创建一个 GitHub App，并在 GitHub Actions 工作流中使用该应用的凭据来生成安装访问令牌。 有关详细信息，请参阅“[在GitHub Actions工作流中使用GitHub应用发出经过身份验证的 API 请求](/zh/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow)”。\n\n1. 创建具有以下访问权限的 personal access token 。 有关详细信息，请参阅“[管理个人访问令牌](/zh/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)”。\n   * 对于 fine-grained personal access token，请授予令牌。\n     * 对创建 Webhook 的存储库的访问权限\n     * 对将运行此工作流的存储库的访问权限\n     * 对存储库 Webhook 权限的写入访问权限\n     * 对存储库变量权限的写入访问权限\n   * 对于 personal access token (classic)，请向令牌授予 `repo` 作用域。\n2. 将你的 personal access token 作为 GitHub Actions 机密存储在你希望运行工作流的存储库中。 有关详细信息，请参阅“[在 GitHub Actions 中使用机密](/zh/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets)”。\n\n## 添加运行脚本的工作流\n\n本部分演示如何使用 GitHub Actions 工作流安全地访问您在上一部分中存储的凭据、设置环境变量，并定期运行脚本来查找和重新传送失败的交付。\n\n将此 GitHub Actions 工作流复制到存储库中希望工作流运行的目录下的 `.github/workflows` YAML 文件中。 按如下所述替换 `Run script` 步骤中的占位符。\n\n```yaml copy annotate\n#\nname: Redeliver failed webhook deliveries\n\n# This workflow runs every 6 hours or when manually triggered.\non:\n  schedule:\n    - cron: '20 */6 * * *'\n  workflow_dispatch:\n\n# This workflow will use the built in `GITHUB_TOKEN` to check out the repository contents. This grants `GITHUB_TOKEN` permission to do that.\npermissions:\n  contents: read\n\n#\njobs:\n  redeliver-failed-deliveries:\n    name: Redeliver failed deliveries\n    runs-on: ubuntu-latest\n    steps:\n      # 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.\n      - name: Check out repo content\n        uses: actions/checkout@v6\n\n      # This step sets up Node.js. The script that this workflow will run uses Node.js.\n      - name: Setup Node.js\n        uses: actions/setup-node@v7\n        with:\n          node-version: '18.x'\n\n      # This step installs the octokit library. The script that this workflow will run uses the octokit library.\n      - name: Install dependencies\n        run: npm install octokit\n\n      # This step sets some environment variables, then runs a script to find and redeliver failed webhook deliveries.\n      # - Replace `YOUR_SECRET_NAME` with the name of the secret where you stored your personal access token.\n      # - Replace `YOUR_REPO_OWNER` with the owner of the repository where the webhook was created.\n      # - Replace `YOUR_REPO_NAME` with the name of the repository where the webhook was created.\n      # - Replace `YOUR_HOOK_ID` with the ID of the webhook.\n      # - 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).\n      \n      - name: Run script\n        env:\n          TOKEN: ${{ secrets.YOUR_SECRET_NAME }}\n          REPO_OWNER: 'YOUR_REPO_OWNER'\n          REPO_NAME: 'YOUR_REPO_NAME'\n          HOOK_ID: 'YOUR_HOOK_ID'\n          LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'\n          \n          WORKFLOW_REPO_NAME: ${{ github.event.repository.name }}\n          WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}\n        run: |\n          node .github/workflows/scripts/redeliver-failed-deliveries.js\n```\n\n## 添加脚本\n\n本部分演示如何编写脚本来查找并重新传送失败的交付。\n\n将此脚本复制到文件`.github/workflows/scripts/redeliver-failed-deliveries.js`中，该文件位于与上面保存GitHub Actions工作流文件的同一存储库中。\n\n```javascript copy annotate\n// 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).\nconst { Octokit } = require(\"octokit\");\n\n//\nasync function checkAndRedeliverWebhooks() {\n  // Get the values of environment variables that were set by the GitHub Actions workflow.\n  const TOKEN = process.env.TOKEN;\n  const REPO_OWNER = process.env.REPO_OWNER;\n  const REPO_NAME = process.env.REPO_NAME;\n  const HOOK_ID = process.env.HOOK_ID;\n  const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;\n  \n  const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO_NAME;\n  const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;\n\n  // Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow.\n  const octokit = new Octokit({ \n    auth: TOKEN,\n  });\n\n  try {\n    // 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.\n    const lastStoredRedeliveryTime = await getVariable({\n      variableName: LAST_REDELIVERY_VARIABLE_NAME,\n      repoOwner: WORKFLOW_REPO_OWNER,\n      repoName: WORKFLOW_REPO_NAME,\n      octokit,\n    });\n    const lastWebhookRedeliveryTime = lastStoredRedeliveryTime || (Date.now() - (24 * 60 * 60 * 1000)).toString();\n\n    // Record the time that this script started redelivering webhooks.\n    const newWebhookRedeliveryTime = Date.now().toString();\n\n    // Get the webhook deliveries that were delivered after `lastWebhookRedeliveryTime`.\n    const deliveries = await fetchWebhookDeliveriesSince({\n      lastWebhookRedeliveryTime,\n      repoOwner: REPO_OWNER,\n      repoName: REPO_NAME,\n      hookId: HOOK_ID,\n      octokit,\n    });\n\n    // Consolidate deliveries that have the same globally unique identifier (GUID). The GUID is constant across redeliveries of the same delivery.\n    let deliveriesByGuid = {};\n    for (const delivery of deliveries) {\n      deliveriesByGuid[delivery.guid]\n        ? deliveriesByGuid[delivery.guid].push(delivery)\n        : (deliveriesByGuid[delivery.guid] = [delivery]);\n    }\n\n    // 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.\n    //\n    // This will prevent duplicate redeliveries if a delivery has failed multiple times.\n    // This will also prevent redelivery of failed deliveries that have already been successfully redelivered.\n    let failedDeliveryIDs = [];\n    for (const guid in deliveriesByGuid) {\n      const deliveries = deliveriesByGuid[guid];\n      const anySucceeded = deliveries.some(\n        (delivery) => delivery.status === \"OK\"\n      );\n      if (!anySucceeded) {\n        failedDeliveryIDs.push(deliveries[0].id);\n      }\n    }\n\n    // Redeliver any failed deliveries.\n    for (const deliveryId of failedDeliveryIDs) {\n      await redeliverWebhook({\n        deliveryId,\n        repoOwner: REPO_OWNER,\n        repoName: REPO_NAME,\n        hookId: HOOK_ID,\n        octokit,\n      });\n    }\n\n    // Update the configuration variable (or create the variable if it doesn't already exist) to store the time that this script started.\n    // This value will be used next time this script runs.\n    await updateVariable({\n      variableName: LAST_REDELIVERY_VARIABLE_NAME,\n      value: newWebhookRedeliveryTime,\n      variableExists: Boolean(lastStoredRedeliveryTime),\n      repoOwner: WORKFLOW_REPO_OWNER,\n      repoName: WORKFLOW_REPO_NAME,\n      octokit,\n    });\n\n    // Log the number of redeliveries.\n    console.log(\n      `Redelivered ${\n        failedDeliveryIDs.length\n      } failed webhook deliveries out of ${\n        deliveries.length\n      } total deliveries since ${new Date(Number(lastWebhookRedeliveryTime))}.`\n    );\n  } catch (error) {\n    // 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.\n    if (error.response) {\n      console.error(\n        `Failed to check and redeliver webhooks: ${error.response.data.message}`\n      );\n    }\n    console.error(error);\n    throw(error);\n  }\n}\n\n// This function will fetch all of the webhook deliveries that were delivered since `lastWebhookRedeliveryTime`.\n// 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).\n//\n// If a page of results includes deliveries that occurred before `lastWebhookRedeliveryTime`,\n// it will store only the deliveries that occurred after `lastWebhookRedeliveryTime` and then stop.\n// Otherwise, it will store all of the deliveries from the page and request the next page.\nasync function fetchWebhookDeliveriesSince({\n  lastWebhookRedeliveryTime,\n  repoOwner,\n  repoName,\n  hookId,\n  octokit,\n}) {\n  const iterator = octokit.paginate.iterator(\n    \"GET /repos/{owner}/{repo}/hooks/{hook_id}/deliveries\",\n    {\n      owner: repoOwner,\n      repo: repoName,\n      hook_id: hookId,\n      per_page: 100,\n      headers: {\n        \"x-github-api-version\": \"2026-03-10\",\n      },\n    }\n  );\n\n  const deliveries = [];\n\n  for await (const { data } of iterator) {\n    const oldestDeliveryTimestamp = new Date(\n      data[data.length - 1].delivered_at\n    ).getTime();\n\n    if (oldestDeliveryTimestamp < lastWebhookRedeliveryTime) {\n      for (const delivery of data) {\n        if (\n          new Date(delivery.delivered_at).getTime() > lastWebhookRedeliveryTime\n        ) {\n          deliveries.push(delivery);\n        } else {\n          break;\n        }\n      }\n      break;\n    } else {\n      deliveries.push(...data);\n    }\n  }\n\n  return deliveries;\n}\n\n// This function will redeliver a failed webhook delivery.\nasync function redeliverWebhook({\n  deliveryId,\n  repoOwner,\n  repoName,\n  hookId,\n  octokit,\n}) {\n  await octokit.request(\n    \"POST /repos/{owner}/{repo}/hooks/{hook_id}/deliveries/{delivery_id}/attempts\",\n    {\n      owner: repoOwner,\n      repo: repoName,\n      hook_id: hookId,\n      delivery_id: deliveryId,\n    }\n  );\n}\n\n// This function gets the value of a configuration variable.\n// If the variable does not exist, the endpoint returns a 404 response and this function returns `undefined`.\nasync function getVariable({ variableName, repoOwner, repoName, octokit }) {\n  try {\n    const {\n      data: { value },\n    } = await octokit.request(\n      \"GET /repos/{owner}/{repo}/actions/variables/{name}\",\n      {\n        owner: repoOwner,\n        repo: repoName,\n        name: variableName,\n      }\n    );\n    return value;\n  } catch (error) {\n    if (error.status === 404) {\n      return undefined;\n    } else {\n      throw error;\n    }\n  }\n}\n\n// 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).\nasync function updateVariable({\n  variableName,\n  value,\n  variableExists,\n  repoOwner,\n  repoName,\n  octokit,\n}) {\n  if (variableExists) {\n    await octokit.request(\n      \"PATCH /repos/{owner}/{repo}/actions/variables/{name}\",\n      {\n        owner: repoOwner,\n        repo: repoName,\n        name: variableName,\n        value: value,\n      }\n    );\n  } else {\n    await octokit.request(\"POST /repos/{owner}/{repo}/actions/variables\", {\n      owner: repoOwner,\n      repo: repoName,\n      name: variableName,\n      value: value,\n    });\n  }\n}\n\n// This will execute the `checkAndRedeliverWebhooks` function.\n(async () => {\n  await checkAndRedeliverWebhooks();\n})();\n\n```\n\n## 测试脚本\n\n可以通过手动触发工作流来测试脚本。 有关详细信息，请参阅 [手动运行工作流](/zh/actions/how-tos/manage-workflow-runs/manually-run-a-workflow) 和 [使用工作流运行日志](/zh/actions/how-tos/monitor-workflows/use-workflow-run-logs)。\n\n## 替代方法\n\n此示例用于 GitHub Actions 安全地存储凭据并按计划运行脚本。 但是，如果你想要在处理 Webhook 传送的服务器上运行此脚本，则可以：\n\n* 以另一种安全方式存储凭据，例如[Azure密钥保管库](https://azure.microsoft.com/products/key-vault)等机密管理器。 你还需要更新脚本以从其新位置访问凭证。\n* 在服务器上按计划运行脚本，例如使用 cron 作业或任务计划程序。\n* 更新脚本以将上次运行时间存储在服务器可以访问和更新的某个位置。 如果选择不将上次运行时存储为 GitHub Actions 机密，则可以删除 API 调用以访问和更新配置变量。"}