О автоматическом повторном развертывании неудачных поставок
В этой статье описывается, как написать скрипт для поиска и повторного размещения неудачных поставок для веб-перехватчика GitHub App веб-перехватчика. Дополнительные сведения о неудачных поставках см. в разделе "Обработка неудачных поставок веб-перехватчика".
В этом примере показано:
- Скрипт, который будет находить и повторно выполнить поставки для GitHub App веб-перехватчика
- Какие учетные данные потребуется скрипту, а также как безопасно хранить учетные данные как секреты GitHub Actions
- Рабочий процесс GitHub Actions, который может безопасно получить доступ к учетным данным и периодически запускать скрипт
В этом примере используется GitHub Actions, но вы также можете запустить этот скрипт на сервере, который обрабатывает поставки веб-перехватчика. Дополнительные сведения см. в разделе "Альтернативные методы".
Хранение учетных данных для скрипта
Конечные точки для поиска и повторного создания веб-перехватчиков требуют веб-маркера JSON, который создается из идентификатора приложения и закрытого ключа для приложения.
Конечные точки для получения и обновления значения переменных среды требуют маркера доступа пользователей personal access token, GitHub App или маркер доступа пользователя GitHub App. В этом примере используется personal access token. Если в репозитории установлен GitHub App и имеется разрешение на запись переменных репозитория, можно изменить этот пример, чтобы создать маркер доступа к установке во время рабочего процесса GitHub Actions вместо использования personal access token. Дополнительные сведения см. в разделе Выполнение запросов API с проверкой подлинности с помощью приложения GitHub в рабочем процессе GitHub Actions.
- Найдите идентификатор приложения для данных GitHub App. Идентификатор приложения можно найти на странице параметров приложения. Идентификатор приложения отличается от идентификатора клиента. Дополнительные сведения о переходе на страницу параметров для GitHub Appсм. в разделе "Изменение регистрации приложения GitHub".
- Сохраните идентификатор приложения из предыдущего шага в виде секрета GitHub Actions в репозитории, в котором требуется запустить рабочий процесс. Дополнительные сведения о хранении секретов см. в разделе "Использование секретов в GitHub Actions".
- Создайте закрытый ключ для приложения. Дополнительные сведения о создании закрытого ключа см. в разделе "Управление закрытыми ключами для приложений GitHub".
- Сохраните закрытый ключ, включая
-----BEGIN RSA PRIVATE KEY-----
и-----END RSA PRIVATE KEY-----
, начиная с предыдущего шага, в качестве секрета GitHub Actions в репозитории, где требуется запустить рабочий процесс. - Создайте personal access token со следующим доступом. Дополнительные сведения см. в разделе Управление личными маркерами доступа.
- Для fine-grained personal access tokenпредоставьте маркер:
- Разрешение на запись в переменные репозитория
- Доступ к репозиторию, в котором будет выполняться этот рабочий процесс.
- Для personal access token (classic)предоставьте маркер области
repo
.
- Для fine-grained personal access tokenпредоставьте маркер:
- Сохраните данные personal access token на предыдущем шаге в виде секрета GitHub Actions в репозитории, где требуется запустить рабочий процесс.
Добавление рабочего процесса, который будет запускать скрипт
В этом разделе показано, как использовать рабочий процесс GitHub Actions для безопасного доступа к учетным данным, хранящимся в предыдущем разделе, задания переменных среды и периодического запуска скрипта для поиска и повторной отправки.
Скопируйте этот рабочий процесс GitHub Actions в файл YAML в каталоге .github/workflows
в репозитории, где требуется запустить рабочий процесс. Замените заполнители на шаге 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@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:
This workflow runs every 6 hours or when manually triggered.
permissions:
contents: read
This workflow will use the built in GITHUB_TOKEN
to check out the repository contents. This grants GITHUB_TOKEN
permission to do that.
jobs:
redeliver-failed-deliveries:
name: Redeliver failed deliveries
runs-on: ubuntu-latest
steps:
- name: Check out repo content
uses: actions/checkout@v4
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: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
This step sets up Node.js. The script that this workflow will run uses Node.js.
- name: Install dependencies
run: npm install octokit
This step installs the octokit library. The script that this workflow will run uses the octokit library.
- 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
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 withGITHUB_
or a number. For more information, see "Хранение сведений в переменных."
#
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");
This script uses GitHub's Octokit SDK to make API requests. For more information, see "Скриптирование с помощью 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;
Get the values of environment variables that were set by the GitHub Actions workflow.
const app = new App({
appId: APP_ID,
privateKey: PRIVATE_KEY,
});
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 octokit = new Octokit({
auth: TOKEN,
});
try {
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 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();
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 newWebhookRedeliveryTime = Date.now().toString();
Record the time that this script started redelivering webhooks.
const deliveries = await fetchWebhookDeliveriesSince({lastWebhookRedeliveryTime, app});
Get the webhook deliveries that were delivered after lastWebhookRedeliveryTime
.
let deliveriesByGuid = {};
for (const delivery of deliveries) {
deliveriesByGuid[delivery.guid]
? deliveriesByGuid[delivery.guid].push(delivery)
: (deliveriesByGuid[delivery.guid] = [delivery]);
}
Consolidate deliveries that have the same globally unique identifier (GUID). The GUID is constant across redeliveries of the same delivery.
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);
}
}
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.
for (const deliveryId of failedDeliveryIDs) {
await redeliverWebhook({deliveryId, app});
}
Redeliver any failed deliveries.
await updateVariable({
variableName: LAST_REDELIVERY_VARIABLE_NAME,
value: newWebhookRedeliveryTime,
variableExists: Boolean(lastStoredRedeliveryTime),
repoOwner: WORKFLOW_REPO_OWNER,
repoName: WORKFLOW_REPO_NAME,
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.
console.log(
`Redelivered ${
failedDeliveryIDs.length
} failed webhook deliveries out of ${
deliveries.length
} total deliveries since ${Date(lastWebhookRedeliveryTime)}.`
);
} catch (error) {
Log the number of redeliveries.
if (error.response) {
console.error(
`Failed to check and redeliver webhooks: ${error.response.data.message}`
);
}
console.error(error);
throw(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.
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 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 "Скриптирование с помощью REST API и JavaScript."
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 redeliverWebhook({deliveryId, app}) {
await app.octokit.request("POST /app/hook/deliveries/{delivery_id}/attempts", {
delivery_id: deliveryId,
});
}
This function will redeliver a failed webhook delivery.
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 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 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 function will update a configuration variable (or create the variable if it doesn't already exist). For more information, see "Хранение сведений в переменных."
(async () => {
await checkAndRedeliverWebhooks();
})();
This will execute the checkAndRedeliverWebhooks
function.
// 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();
})();
Тестирование скрипта
Вы можете вручную активировать рабочий процесс для тестирования скрипта. Дополнительные сведения см. в разделе "[AUTOTITLE" и "Запуск рабочего процесса вручную](/actions/monitoring-and-troubleshooting-workflows/using-workflow-run-logs)".
Альтернативные методы
В этом примере используется GitHub Actions для безопасного хранения учетных данных и запуска скрипта по расписанию. Однако если вы предпочитаете запускать этот скрипт на сервере, чем обрабатывает поставки веб-перехватчиков, вы можете:
- Сохраните учетные данные в другом безопасном режиме, например диспетчер секретов, например хранилище ключей Azure. Вам также потребуется обновить скрипт, чтобы получить доступ к учетным данным из нового расположения.
- Запустите скрипт по расписанию на сервере, например с помощью задания cron или планировщика задач.
- Обновите скрипт, чтобы сохранить время последнего выполнения где-либо, к которому может обращаться и обновлять сервер. Если вы решили не хранить время последнего выполнения в виде секрета GitHub Actions, вам не нужно использовать personal access token, и можно удалить вызовы API для доступа к переменной конфигурации и обновить ее.