关于自动重新投递失败的投递
本文介绍如何编写脚本来查找并重新投递 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 请求。
- 查找您的 GitHub App 的应用 ID。您可以在应用的设置页面上找到应用 ID。应用 ID 与客户端 ID 不同。有关如何导航到 GitHub App 设置页面的更多信息,请参阅 修改 GitHub App 注册信息。
- 将上一步的应用 ID 作为 GitHub Actions secret 存储在您希望运行工作流的仓库中。有关存储 secret 的更多信息,请参阅 在 GitHub Actions 中使用 secret。
- 为您的应用生成私钥。有关生成私钥的更多信息,请参阅 管理 GitHub App 私钥。
- 将上一步的私钥(包括
-----BEGIN RSA PRIVATE KEY-----和-----END RSA PRIVATE KEY-----)作为 GitHub Actions secret 存储在您希望运行工作流的仓库中。 - 创建具有以下访问权限的个人访问令牌。更多信息请参阅管理个人访问令牌。
- 对于细粒度个人访问令牌,请授予该令牌
- 对仓库变量的写权限
- 对运行此工作流的仓库的访问权限
- 对于传统个人访问令牌,请授予该令牌
repo范围。
- 对于细粒度个人访问令牌,请授予该令牌
- 将上一步的个人访问令牌作为 GitHub Actions secret 存储在您希望运行工作流的仓库中。
添加将运行脚本的工作流
本节展示如何使用 GitHub Actions 工作流安全访问前一节中存储的凭证、设置环境变量,并定期运行脚本以查找并重新投递失败的投递。
将以下 GitHub Actions 工作流复制到您希望工作流运行的仓库的.github/workflows目录下的 YAML 文件中。按照下文所述替换 Run script 步骤中的占位符。
#
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
name: Redeliver failed webhook deliverieson:
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 工作流文件的同一仓库中。
// 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();
})();
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 调用。