关于自动重新交付失败交付
本文介绍如何编写脚本查找并重新交付 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 密钥,存储在您希望工作流程运行的存储库中。有关存储密钥的更多信息,请参阅“在 GitHub Actions 中使用密钥”。
-
为您的应用生成私钥。有关生成私钥的更多信息,请参阅“管理 GitHub App 的私钥”。
-
将上一步中的私钥(包括
-----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 Actions 工作流程文件相同的存储库中的名为 .github/workflows/scripts/redeliver-failed-deliveries.js
的文件中。
// 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 调用。