{"meta":{"title":"自动重新传送 GitHub App Webhook 的失败交付","intro":"可以编写脚本来处理 GitHub App 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-github-app-webhook","title":"为 GitHub App 自动重新传递"}],"documentType":"article"},"body":"# 自动重新传送 GitHub App Webhook 的失败交付\n\n可以编写脚本来处理 GitHub App Webhook 的失败传递。\n\n## 关于自动重新传送失败交付\n\n本文介绍如何编写脚本，以查找并重新传递 GitHub App Webhook 的失败传递。 有关失败的交付的详细信息，请参阅“[处理失败的 Webhook 交付](/zh/webhooks/using-webhooks/handling-failed-webhook-deliveries)”。\n\n这个示例向您展示了：\n\n* 用于查找并重新传递 GitHub App Webhook 失败传递的脚本\n* 脚本需要哪些凭据，以及如何将凭据安全地存储为 GitHub Actions 机密\n* 一个可以安全地访问凭据并定期运行脚本的 GitHub Actions 工作流\n\n此示例使用 GitHub Actions，但也可以在处理 Webhook 传送的服务器上运行此脚本。 有关详细信息，请参阅[替代方法](#alternative-methods)。\n\n## 为脚本存储凭证\n\n用于查找和重新传送失败的 Webhook 的端点需要 JSON Web 令牌，该令牌从应用的应用 ID 和私钥生成。\n\n用于获取和更新环境变量值的端点需要一个personal access tokenGitHub App安装访问令牌或GitHub App用户访问令牌。 此示例使用personal access token。 如果您的 GitHub App 已安装在将运行此工作流的仓库中，并且具有写入仓库变量的权限，则可以修改此示例，以便在 GitHub Actions 工作流期间创建安装访问令牌，而不是使用 personal access token。 有关详细信息，请参阅“[在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. 查找你的 GitHub App应用 ID。 可以在应用的设置页上找到应用 ID。 应用 ID 不同于客户端 ID。 有关导航到设置 GitHub App页的详细信息，请参阅 [修改GitHub应用注册](/zh/apps/maintaining-github-apps/modifying-a-github-app-registration#navigating-to-your-github-app-settings)。\n2. 将上一 GitHub Actions 步骤中的应用 ID 存储为要在其中运行工作流的存储库中的机密。 有关存储机密的详细信息，请参阅“[在 GitHub Actions 中使用机密](/zh/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets)”。\n3. 为应用生成私钥。 有关生成私钥的详细信息，请参阅 [管理GitHub应用的私钥](/zh/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps)。\n4. 将私钥（包括 `-----BEGIN RSA PRIVATE KEY-----` 和 `-----END RSA PRIVATE KEY-----`）从上一 GitHub Actions 步存储为要在其中运行工作流的存储库中的机密。\n5. 创建具有以下访问权限的 personal access token 。 有关详细信息，请参阅“[管理个人访问令牌](/zh/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)”。\n   * 对于 fine-grained personal access token，请授予令牌。\n     * 对存储库变量权限的写入访问权限\n     * 对将运行此工作流的存储库的访问权限\n   * 对于 personal access token (classic)，请向令牌授予 `repo` 作用域。\n6. 将上一步中的 personal access token 作为 GitHub Actions 密钥存储在要运行该工作流的仓库中。\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: '40 */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: '20.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_APP_ID_SECRET_NAME` with the name of the secret where you stored your app ID.\n      # - Replace `YOUR_PRIVATE_KEY_SECRET_NAME` with the name of the secret where you stored your private key.\n      # - Replace `YOUR_TOKEN_SECRET_NAME` with the name of the secret where you stored your personal access token.\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          APP_ID: ${{ secrets.YOUR_APP_ID_SECRET_NAME }}\n          PRIVATE_KEY: ${{ secrets.YOUR_PRIVATE_KEY_SECRET_NAME }}\n          TOKEN: ${{ secrets.YOUR_TOKEN_SECRET_NAME }}\n          LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'\n          \n          WORKFLOW_REPO: ${{ github.event.repository.name }}\n          WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}\n        run: |\n          node .github/workflows/scripts/redeliver-failed-deliveries.mjs\n```\n\n## 添加脚本\n\n本部分演示如何编写脚本来查找并重新传送失败的交付。\n\n将此脚本复制到文件`.github/workflows/scripts/redeliver-failed-deliveries.mjs`中，该文件位于与上面保存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).\nimport { App, Octokit } from \"octokit\";\n\n//\nasync function checkAndRedeliverWebhooks() {\n  // Get the values of environment variables that were set by the GitHub Actions workflow.\n  const APP_ID = process.env.APP_ID;\n  const PRIVATE_KEY = process.env.PRIVATE_KEY;\n  const TOKEN = process.env.TOKEN;\n  const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;\n  \n  const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO;\n  const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;\n\n  // Create an instance of the octokit `App` using the app ID and private key values that were set in the GitHub Actions workflow.\n  //\n  // This will be used to make API requests to the webhook-related endpoints.\n  const app = new App({\n    appId: APP_ID,\n    privateKey: PRIVATE_KEY,\n  });\n\n  // Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow.\n  //\n  // This will be used to update the configuration variable that stores the last time that this script ran.\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({lastWebhookRedeliveryTime, app});\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({deliveryId, app});\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 ${Date(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({lastWebhookRedeliveryTime, app}) {\n  const iterator = app.octokit.paginate.iterator(\n    \"GET /app/hook/deliveries\",\n    {\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({deliveryId, app}) {\n  await app.octokit.request(\"POST /app/hook/deliveries/{delivery_id}/attempts\", {\n    delivery_id: deliveryId,\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 机密，则无需使用 a personal access token，并且可以删除 API 调用来访问和更新配置变量。"}