Sobre a reentrega automática de entregas com falha
Este artigo descreve como escrever um script para localizar e reenviar entregas com falha para um webhook de repositório. Para obter mais informações sobre entregas com falha, consulte "Como lidar com falhas de entrega de webhook".
Este exemplo mostra:
- Um script que localizará e reenviará entregas com falha para um webhook de repositório
- Quais credenciais seu script precisará e como armazenar as credenciais com segurança como segredos do GitHub Actions
- Um fluxo de trabalho do GitHub Actions que pode acessar com segurança suas credenciais e executar o script periodicamente
Este exemplo usa o GitHub Actions, mas você também pode executar esse script em seu servidor que lida com entregas de webhooks. Para obter mais informações, consulte "Métodos alternativos".
Armazenamento de credenciais para o script
O GITHUB_TOKEN
interno não tem permissões suficientes para reentregar webhooks. Em vez de usar GITHUB_TOKEN
, este exemplo usa um personal access token. Como alternativa, em vez de criar um personal access token, você pode criar um GitHub App e usar as credenciais do aplicativo para criar um token de acesso de instalação durante o fluxo de trabalho do GitHub Actions. Para obter mais informações, confira "Fazer solicitações de API autenticadas com um Aplicativo do GitHub em um fluxo de trabalho do GitHub Actions".
-
Crie um personal access token com o acesso a seguir. Para obter mais informações, confira "Gerenciar seus tokens de acesso pessoal".
- Para um fine-grained personal access token, conceda ao token:
- Acesso ao repositório onde seu webhook foi criado
- Acesso ao repositório em que esse fluxo de trabalho será executado
- Acesso para gravação à permissão de webhooks do repositório
- Permissão de acesso de gravação a variáveis do repositório
- Para um personal access token (classic), conceda ao token o escopo
repo
.
- Para um fine-grained personal access token, conceda ao token:
-
Armazene seus personal access token como um segredo de GitHub Actions no repositório em que você deseja que o fluxo de trabalho seja executado. Para obter mais informações, confira "Usar segredos em ações do GitHub".
Adicionando um fluxo de trabalho que executará o script
Esta seção demonstra como você pode usar um fluxo de trabalho do GitHub Actions para acessar com segurança as credenciais armazenadas na seção anterior, definir variáveis de ambiente e executar periodicamente um script para localizar e reentregar entregas com falha.
Copie esse fluxo de trabalho do GitHub Actions em um arquivo YAML no diretório .github/workflows
no repositório em que você deseja que o fluxo de trabalho seja executado. Substitua os espaços reservados na etapa Run script
conforme descrito abaixo.
# name: Redeliver failed webhook deliveries # This workflow runs every 6 hours or when manually triggered. on: schedule: - cron: '20 */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_REPO_OWNER` with the owner of the repository where the webhook was created. # - Replace `YOUR_REPO_NAME` with the name of the repository 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)." # - Replace `YOUR_HOSTNAME` with the name of sua instância do GitHub Enterprise Server. - name: Run script env: TOKEN: ${{ secrets.YOUR_SECRET_NAME }} REPO_OWNER: 'YOUR_REPO_OWNER' REPO_NAME: 'YOUR_REPO_NAME' HOOK_ID: 'YOUR_HOOK_ID' LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME' HOSTNAME: 'YOUR_HOSTNAME' 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: '20 */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:
TOKEN: ${{ secrets.YOUR_SECRET_NAME }}
REPO_OWNER: 'YOUR_REPO_OWNER'
REPO_NAME: 'YOUR_REPO_NAME'
HOOK_ID: 'YOUR_HOOK_ID'
LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'
HOSTNAME: 'YOUR_HOSTNAME'
WORKFLOW_REPO_NAME: ${{ 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_SECRET_NAME
with the name of the secret where you stored your personal access token. - Replace
YOUR_REPO_OWNER
with the owner of the repository where the webhook was created. - Replace
YOUR_REPO_NAME
with the name of the repository 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 withGITHUB_
or a number. For more information, see "Store information in variables." - Replace
YOUR_HOSTNAME
with the name of sua instância do GitHub Enterprise Server.
#
name: Redeliver failed webhook deliveries
# This workflow runs every 6 hours or when manually triggered.
on:
schedule:
- cron: '20 */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_REPO_OWNER` with the owner of the repository where the webhook was created.
# - Replace `YOUR_REPO_NAME` with the name of the repository 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)."
# - Replace `YOUR_HOSTNAME` with the name of sua instância do GitHub Enterprise Server.
- name: Run script
env:
TOKEN: ${{ secrets.YOUR_SECRET_NAME }}
REPO_OWNER: 'YOUR_REPO_OWNER'
REPO_NAME: 'YOUR_REPO_NAME'
HOOK_ID: 'YOUR_HOOK_ID'
LAST_REDELIVERY_VARIABLE_NAME: 'YOUR_LAST_REDELIVERY_VARIABLE_NAME'
HOSTNAME: 'YOUR_HOSTNAME'
WORKFLOW_REPO_NAME: ${{ github.event.repository.name }}
WORKFLOW_REPO_OWNER: ${{ github.repository_owner }}
run: |
node .github/workflows/scripts/redeliver-failed-deliveries.js
Adicionando o script
Esta seção demonstra como você pode escrever um script para localizar e reenviar entregas com falha.
Copie esse script em um arquivo chamado .github/workflows/scripts/redeliver-failed-deliveries.js
no mesmo repositório em que você salvou o arquivo de fluxo de trabalho GitHub Actions acima.
// 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 REPO_OWNER = process.env.REPO_OWNER; const REPO_NAME = process.env.REPO_NAME; const HOOK_ID = process.env.HOOK_ID; const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME; const HOSTNAME = process.env.HOSTNAME; 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 and hostname values that were set in the GitHub Actions workflow. const octokit = new Octokit({ baseUrl: "http(s)://HOSTNAME/api/v3", 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, repoOwner: REPO_OWNER, repoName: REPO_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, repoOwner: REPO_OWNER, repoName: REPO_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, repoOwner, repoName, hookId, octokit, }) { const iterator = octokit.paginate.iterator( "GET /repos/{owner}/{repo}/hooks/{hook_id}/deliveries", { owner: repoOwner, repo: repoName, 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, repoOwner, repoName, hookId, octokit, }) { await octokit.request( "POST /repos/{owner}/{repo}/hooks/{hook_id}/deliveries/{delivery_id}/attempts", { owner: repoOwner, repo: repoName, 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");
This script uses GitHub's Octokit SDK to make API requests. For more information, see "Scripts com a API REST e o JavaScript."
async function checkAndRedeliverWebhooks() {
const TOKEN = process.env.TOKEN;
const REPO_OWNER = process.env.REPO_OWNER;
const REPO_NAME = process.env.REPO_NAME;
const HOOK_ID = process.env.HOOK_ID;
const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;
const HOSTNAME = process.env.HOSTNAME;
const WORKFLOW_REPO_NAME = process.env.WORKFLOW_REPO_NAME;
const WORKFLOW_REPO_OWNER = process.env.WORKFLOW_REPO_OWNER;
Get the values of environment variables that were set by the GitHub Actions workflow.
const octokit = new Octokit({
baseUrl: "http(s)://HOSTNAME/api/v3",
auth: TOKEN,
});
try {
Create an instance of Octokit
using the token and hostname values that were set in the GitHub Actions workflow.
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,
repoOwner: REPO_OWNER,
repoName: REPO_NAME,
hookId: HOOK_ID,
octokit,
});
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,
repoOwner: REPO_OWNER,
repoName: REPO_NAME,
hookId: HOOK_ID,
octokit,
});
}
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,
repoOwner,
repoName,
hookId,
octokit,
}) {
const iterator = octokit.paginate.iterator(
"GET /repos/{owner}/{repo}/hooks/{hook_id}/deliveries",
{
owner: repoOwner,
repo: repoName,
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 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 "Scripts com a API REST e o 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,
repoOwner,
repoName,
hookId,
octokit,
}) {
await octokit.request(
"POST /repos/{owner}/{repo}/hooks/{hook_id}/deliveries/{delivery_id}/attempts",
{
owner: repoOwner,
repo: repoName,
hook_id: hookId,
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 "Store information in variables."
(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 { 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 REPO_OWNER = process.env.REPO_OWNER;
const REPO_NAME = process.env.REPO_NAME;
const HOOK_ID = process.env.HOOK_ID;
const LAST_REDELIVERY_VARIABLE_NAME = process.env.LAST_REDELIVERY_VARIABLE_NAME;
const HOSTNAME = process.env.HOSTNAME;
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 and hostname values that were set in the GitHub Actions workflow.
const octokit = new Octokit({
baseUrl: "http(s)://HOSTNAME/api/v3",
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,
repoOwner: REPO_OWNER,
repoName: REPO_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,
repoOwner: REPO_OWNER,
repoName: REPO_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,
repoOwner,
repoName,
hookId,
octokit,
}) {
const iterator = octokit.paginate.iterator(
"GET /repos/{owner}/{repo}/hooks/{hook_id}/deliveries",
{
owner: repoOwner,
repo: repoName,
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,
repoOwner,
repoName,
hookId,
octokit,
}) {
await octokit.request(
"POST /repos/{owner}/{repo}/hooks/{hook_id}/deliveries/{delivery_id}/attempts",
{
owner: repoOwner,
repo: repoName,
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();
})();
Testando o script
Você pode acionar manualmente seu fluxo de trabalho para testar o script. Para obter mais informações, confira "Executar um fluxo de trabalho manualmente" e "Usando logs de execução de fluxo de trabalho."
Métodos alternativos
Este exemplo usou o GitHub Actions para armazenar credenciais com segurança e executar o script em um cronograma. No entanto, se preferir executar esse script no seu servidor que lida com entregas de webhooks, você poderá:
- Armazene as credenciais de outra maneira segura, como um gerenciador de segredos, como o cofre de chaves do Azure. Também será necessário atualizar o script para acessar as credenciais do novo local.
- Execute o script em um cronograma no seu servidor, por exemplo, usando um trabalho cron ou um agendador de tarefas.
- Atualize o script para armazenar o último tempo de execução em algum lugar que seu servidor possa acessar e atualizar. Se você optar por não armazenar o último tempo de execução como um segredo do GitHub Actions, poderá remover as chamadas de API para acessar e atualizar a variável de configuração.