关于自动重新传递传递失败
本文介绍如何编写脚本来查找并重新传递组织 Webhook 的传递失败。有关传递失败的详细信息,请参阅“处理失败的 Webhook 传递”。
此示例向你展示
- 查找并重新传递组织 Webhook 的传递失败的脚本
- 你的脚本需要哪些凭据,以及如何将凭据安全地存储为 GitHub Actions 秘钥
- 可以安全地访问你的凭据并定期运行脚本的 GitHub Actions 工作流
此示例使用 GitHub Actions,但你也可以在处理 Webhook 传递的服务器上运行此脚本。有关详细信息,请参阅“备用方法”。
为脚本存储凭据
内置的 GITHUB_TOKEN
没有足够的权限来重新传递 Webhook。此示例使用个人访问令牌,而不是使用 GITHUB_TOKEN
。或者,你可以创建一个 GitHub 应用,而不是创建个人访问令牌,并使用该应用的凭据在 GitHub Actions 工作流期间创建一个安装访问令牌。有关详细信息,请参阅“在 GitHub Actions 工作流中使用 GitHub 应用进行经过身份验证的 API 请求”。
-
使用以下访问权限创建一个个人访问令牌。有关详细信息,请参阅“管理你的个人访问令牌”。
- 对于细粒度的个人访问令牌
- 将资源所有者设置为创建 Webhook 的组织
- 授予令牌访问此工作流将运行其中的存储库的权限
- 授予令牌对组织 Webhook 权限的写入访问权限
- 授予令牌对存储库变量权限的写入访问权限
- 对于个人访问令牌(经典),授予令牌
admin:org_hook
和repo
范围。
- 对于细粒度的个人访问令牌
-
将你的个人访问令牌存储为 GitHub Actions 秘钥,存储在你要运行工作流的存储库中。有关详细信息,请参阅“在 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: '15 */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_SECRET_NAME` with the name of the secret where you stored your personal access token. # - Replace `YOUR_ORGANIZATION_NAME` with the name of the organization where the webhook was created. # - Replace `YOUR_HOOK_ID` with the ID of the webhook. # - 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: TOKEN: ${{ secrets.YOUR_SECRET_NAME }} ORGANIZATION_NAME: 'YOUR_ORGANIZATION_NAME' HOOK_ID: 'YOUR_HOOK_ID' LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME' WORKFLOW_REPO_NAME: ${{ 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: '15 */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:
TOKEN: ${{ secrets.YOUR_SECRET_NAME }}
ORGANIZATION_NAME: 'YOUR_ORGANIZATION_NAME'
HOOK_ID: 'YOUR_HOOK_ID'
LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'
WORKFLOW_REPO_NAME: ${{ github.event.repository.name }}
WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}
run: |
node .github/workflows/scripts/redeliver-failed-deliveries.js
此步骤设置一些环境变量,然后运行脚本以查找并重新发送失败的 webhook 投递。
- 将
YOUR_SECRET_NAME
替换为你存储个人访问令牌的秘钥的名称。 - 将
YOUR_ORGANIZATION_NAME
替换为创建 webhook 的组织的名称。 - 将
YOUR_HOOK_ID
替换为 webhook 的 ID。 - 将
YOUR_LAST_REDELIVERY_VARIABLE_NAME
替换为你想要用于配置变量的名称,该变量将存储在此工作流存储的存储库中。该名称可以是任何仅包含字母数字字符和_
的字符串,并且不能以GITHUB_
或数字开头。有关详细信息,请参阅“变量”。
#
name: Redeliver failed webhook deliveries
# This workflow runs every 6 hours or when manually triggered.
on:
schedule:
- cron: '15 */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_SECRET_NAME` with the name of the secret where you stored your personal access token.
# - Replace `YOUR_ORGANIZATION_NAME` with the name of the organization where the webhook was created.
# - Replace `YOUR_HOOK_ID` with the ID of the webhook.
# - 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:
TOKEN: ${{ secrets.YOUR_SECRET_NAME }}
ORGANIZATION_NAME: 'YOUR_ORGANIZATION_NAME'
HOOK_ID: 'YOUR_HOOK_ID'
LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'
WORKFLOW_REPO_NAME: ${{ 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 { Octokit } = require("octokit"); // async function checkAndRedeliverWebhooks() { // Get the values of environment variables that were set by the GitHub Actions workflow. const TOKEN = process.env.TOKEN; const ORGANIZATION_NAME = process.env.ORGANIZATION_NAME; const HOOK_ID = process.env.HOOK_ID; const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME; const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO_NAME; const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER; // Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow. 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, organizationName: ORGANIZATION_NAME, hookId: HOOK_ID, octokit, }); // 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, organizationName: ORGANIZATION_NAME, hookId: HOOK_ID, octokit, }); } // 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, organizationName, hookId, octokit, }) { const iterator = octokit.paginate.iterator( "GET /orgs/{org}/hooks/{hook_id}/deliveries", { org: organizationName, hook_id: hookId, 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, organizationName, hookId, octokit, }) { await octokit.request( "POST /orgs/{org}/hooks/{hook_id}/deliveries/{delivery_id}/attempts", { org: organizationName, hook_id: hookId, 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 { Octokit } = require("octokit");
此脚本使用 GitHub 的 Octokit SDK 来发出 API 请求。有关更多信息,请参阅“使用 REST API 和 JavaScript 编写脚本”。
async function checkAndRedeliverWebhooks() {
const TOKEN = process.env.TOKEN;
const ORGANIZATION_NAME = process.env.ORGANIZATION_NAME;
const HOOK_ID = process.env.HOOK_ID;
const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;
const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO_NAME;
const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;
获取由 GitHub Actions 工作流设置的环境变量的值。
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,
organizationName: ORGANIZATION_NAME,
hookId: HOOK_ID,
octokit,
});
获取在 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,
organizationName: ORGANIZATION_NAME,
hookId: HOOK_ID,
octokit,
});
}
重新传递任何失败的传递。
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,
organizationName,
hookId,
octokit,
}) {
const iterator = octokit.paginate.iterator(
"GET /orgs/{org}/hooks/{hook_id}/deliveries",
{
org: organizationName,
hook_id: hookId,
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,
organizationName,
hookId,
octokit,
}) {
await octokit.request(
"POST /orgs/{org}/hooks/{hook_id}/deliveries/{delivery_id}/attempts",
{
org: organizationName,
hook_id: hookId,
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 { Octokit } = require("octokit");
//
async function checkAndRedeliverWebhooks() {
// Get the values of environment variables that were set by the GitHub Actions workflow.
const TOKEN = process.env.TOKEN;
const ORGANIZATION_NAME = process.env.ORGANIZATION_NAME;
const HOOK_ID = process.env.HOOK_ID;
const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;
const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO_NAME;
const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;
// Create an instance of `Octokit` using the token values that were set in the GitHub Actions workflow.
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,
organizationName: ORGANIZATION_NAME,
hookId: HOOK_ID,
octokit,
});
// 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,
organizationName: ORGANIZATION_NAME,
hookId: HOOK_ID,
octokit,
});
}
// 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,
organizationName,
hookId,
octokit,
}) {
const iterator = octokit.paginate.iterator(
"GET /orgs/{org}/hooks/{hook_id}/deliveries",
{
org: organizationName,
hook_id: hookId,
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,
organizationName,
hookId,
octokit,
}) {
await octokit.request(
"POST /orgs/{org}/hooks/{hook_id}/deliveries/{delivery_id}/attempts",
{
org: organizationName,
hook_id: hookId,
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 调用。