关于失败投递的自动重试
本文介绍如何编写脚本来查找和重新投递 GitHub 应用 Webhook 的失败投递。有关失败投递的更多信息,请参阅“处理失败的 Webhook 投递”。
此示例演示
- 一个可以查找和重新投递 GitHub 应用 Webhook 失败投递的脚本
- 脚本所需的凭据以及如何将凭据安全地存储为 GitHub Actions 密钥
- 一个 GitHub Actions 工作流程,可以安全地访问您的凭据并定期运行脚本
此示例使用 GitHub Actions,但您也可以在处理 Webhook 投递的服务器上运行此脚本。有关更多信息,请参阅“替代方法”。
存储脚本的凭据
查找和重新投递失败 Webhook 的端点需要 JSON Web 令牌,该令牌由应用的应用 ID 和私钥生成。
获取和更新环境变量值的端点需要个人访问令牌、GitHub 应用安装访问令牌或 GitHub 应用用户访问令牌。此示例使用个人访问令牌。如果您的 GitHub 应用已安装在此工作流程将运行的仓库中,并且具有写入仓库变量的权限,则可以修改此示例以在 GitHub Actions 工作流程期间创建安装访问令牌,而不是使用个人访问令牌。有关更多信息,请参阅“在 GitHub Actions 工作流程中使用 GitHub 应用进行身份验证的 API 请求”。
- 查找您的 GitHub 应用的应用 ID。您可以在应用的设置页面上找到应用 ID。应用 ID 与客户端 ID 不同。有关导航到 GitHub 应用设置页面的更多信息,请参阅“修改 GitHub 应用注册”。
- 将上一步中的应用 ID 存储为 GitHub Actions 密钥,存储在您希望工作流程运行的仓库中。有关存储密钥的更多信息,请参阅“在 GitHub Actions 中使用密钥”。
- 为您的应用生成私钥。有关生成私钥的更多信息,请参阅“管理 GitHub 应用的私钥”。
- 将上一步中的私钥(包括 `-----BEGIN RSA PRIVATE KEY-----` 和 `-----END RSA PRIVATE KEY-----`)存储为 GitHub Actions 密钥,存储在您希望工作流程运行的仓库中。
- 创建具有以下访问权限的个人访问令牌。有关更多信息,请参阅“管理您的个人访问令牌”。
- 对于细粒度的个人访问令牌,请授予令牌
- 写入仓库变量权限
- 访问此工作流程将运行的仓库
- 对于个人访问令牌(经典版),请授予令牌 `repo` 作用域。
- 对于细粒度的个人访问令牌,请授予令牌
- 将上一步中的个人访问令牌存储为 GitHub Actions 密钥,存储在您希望工作流程运行的仓库中。
添加将运行脚本的工作流程
本节演示如何使用 GitHub Actions 工作流程安全地访问您在上一步中存储的凭据、设置环境变量以及定期运行脚本来查找和重新投递失败的投递。
将此 GitHub Actions 工作流程复制到您希望工作流程运行的仓库中 `.github/workflows` 目录下的 YAML 文件中。替换下面所述的 `运行脚本` 步骤中的占位符。
# 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@v4 # 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: '18.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.js
name: Redeliver failed webhook deliveries
on:
schedule:
- cron: '40 */6 * * *'
workflow_dispatch:
此工作流程每 6 小时运行一次,或在手动触发时运行。
permissions:
contents: read
此工作流程将使用内置的 `GITHUB_TOKEN` 来检出仓库内容。这授予 `GITHUB_TOKEN` 执行此操作的权限。
jobs:
redeliver-failed-deliveries:
name: Redeliver failed deliveries
runs-on: ubuntu-latest
steps:
- name: Check out repo content
uses: actions/checkout@v4
此工作流程将运行存储在仓库中的脚本。此步骤检出仓库内容,以便工作流程可以访问脚本。
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.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.js
此步骤设置一些环境变量,然后运行脚本来查找和重新投递失败的 Webhook 投递。
- 将 `YOUR_APP_ID_SECRET_NAME` 替换为您存储应用 ID 的密钥的名称。
- 将 `YOUR_PRIVATE_KEY_SECRET_NAME` 替换为您存储私钥的密钥的名称。
- 将 `YOUR_TOKEN_SECRET_NAME` 替换为您存储个人访问令牌的密钥的名称。
- 将 `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@v4
# 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: '18.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.js
添加脚本
本节演示如何编写脚本来查找和重新投递失败的投递。
将此脚本复制到名为 `.github/workflows/scripts/redeliver-failed-deliveries.js` 的文件中,该文件与您在上面保存的 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)." const { App, Octokit } = require("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": "2022-11-28", }, } ); 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(); })();
const { App, Octokit } = require("octokit");
此脚本使用 GitHub 的 Octokit SDK 来发出 API 请求。有关更多信息,请参阅“使用 REST API 和 JavaScript 编写脚本”。
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 {
使用在 GitHub Actions 工作流程中设置的令牌值创建 `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 的投递成功投递,则获取具有该 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": "2022-11-28",
},
}
);
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()
方法迭代分页结果。更多信息,请参见“使用 REST API 和 JavaScript 编写脚本”。
如果一页结果包含在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)."
const { App, Octokit } = require("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": "2022-11-28",
},
}
);
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 密钥保管库这样的密钥管理器。您还需要更新脚本以从其新位置访问凭据。
- 例如,使用 cron 作业或任务计划程序,按计划在您的服务器上运行脚本。
- 更新脚本以将上次运行时间存储在您的服务器可以访问和更新的位置。如果您选择不将上次运行时间存储为 GitHub Actions 密钥,则无需使用个人访问令牌,您可以删除访问和更新配置变量的 API 调用。