Skip to main content

Automatisches erneutes Zustellen fehlgeschlagener Zustellungen für einen Organisationswebhook

Du kannst ein Skript schreiben, um fehlgeschlagene Zustellungen eines Organisationswebhooks zu verarbeiten.

Informationen zur automatischen erneuten Zustellung fehlgeschlagener Zustellungen

In diesem Artikel wird beschrieben, wie du ein Skript schreibst, um fehlgeschlagene Zustellungen für einen Organisationswebhook zu finden und erneut zu senden. Weitere Informationen zu fehlgeschlagenen Zustellungen findest du unter Bearbeitung fehlgeschlagener Webhook-Zustellungen.

In diesem Beispiel wird Folgendes angezeigt:

  • Ein Skript, das fehlgeschlagene Zustellungen für einen Organisationswebhook finden und erneut senden kann
  • Welche Anmeldeinformationen für dein Skript erforderlich sind und wie du sie sicher als GitHub Actions-Geheimnisse speichern kannst
  • Ein GitHub Actions-Workflow, der sicher auf deine Anmeldeinformationen zugreifen und das Skript regelmäßig ausführen kann

In diesem Beispiel werden GitHub Actions verwendet. Du kannst dieses Skript aber auch auf deinem Server ausführen, der Webhookübermittlungen verarbeitet. Weitere Informationen findest du unter Alternative Methoden.

Speichern von Anmeldeinformationen für das Skript

Das integrierte GITHUB_TOKEN verfügt nicht über ausreichende Berechtigungen, um Webhooks erneut zuzustellen. In diesem Beispiel wird statt GITHUB_TOKEN ein personal access token verwendet. Alternativ dazu können Sie anstelle eines personal access token eine GitHub App erstellen und die Anmeldeinformationen der App verwenden, um während des GitHub Actions-Workflows ein Zugriffstoken für die Installation zu erstellen. Weitere Informationen findest du unter Authentifizierte API-Anforderungen mit einer GitHub-App in einem GitHub Actions-Workflow.

  1. Erstellen Sie eine personal access token mit dem folgenden Zugriff: Weitere Informationen findest du unter Verwalten deiner persönlichen Zugriffstoken.

    • Für ein fine-grained personal access token:
      • Festlegen des Ressourcenbesitzers auf die Organisation, in der dein Webhook erstellt wurde
      • Erteilen des Tokenzugriffs auf das Repository, in dem dieser Workflow ausgeführt wird
      • Erteilen des Token-Schreibzugriffs auf die Webhooks-Berechtigung der Organisation
      • Erteilen des Token-Schreibzugriffs auf die Repositoryvariablen-Berechtigung
    • Für eine personal access token (classic) weisen Sie dem Token den admin:org_hook- und repo-Bereich zu:
  2. Speichern Sie Ihre personal access token als GitHub Actions-Geheimnis in dem Repository, in dem der Workflow ausgeführt werden soll. Weitere Informationen findest du unter Verwenden von Geheimnissen in GitHub-Aktionen.

Hinzufügen eines Workflows, der das Skript ausführt

In diesem Abschnitt wird veranschaulicht, wie du einen GitHub Actions-Workflow verwenden kannst, um sicher auf die Anmeldeinformationen zuzugreifen, die du im vorherigen Abschnitt gespeichert hast, und um Umgebungsvariablen festzulegen und regelmäßig ein Skript auszuführen, um fehlgeschlagene Zustellungen zu finden und erneut zu senden.

Kopieren Sie diesen GitHub Actions Workflow in eine YAML-Datei in das .github/workflows-Verzeichnis des Repositorys, in dem der Workflow ausgeführt werden soll. Ersetzen Sie die Platzhalter im Run script-Schritt wie unten beschrieben.

YAML
name: Redeliver failed webhook deliveries
on:
  schedule:
    - cron: '15 */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@v3
        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 }}
          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

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 "Variablen."
#
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@v3
        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

Hinzufügen des Skripts

In diesem Abschnitt wird veranschaulicht, wie du ein Skript schreiben kannst, um fehlgeschlagene Zustellungen zu finden und erneut zu senden.

Kopieren Sie dieses Skript in eine Datei mit dem Namen .github/workflows/scripts/redeliver-failed-deliveries.js, die in demselben Repository aufgerufen wird, in dem Sie oben die GitHub Actions-Workflow-Datei gespeichert haben.

JavaScript
const { Octokit } = require("octokit");

This script uses GitHub's Octokit SDK to make API requests. For more information, see "Skripterstellung mit der REST-API und 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;

Get the values of environment variables that were set by the GitHub Actions workflow.

  const octokit = new Octokit({ 
    auth: TOKEN,
  });
  try {

Create an instance of Octokit using the token 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,
      organizationName: ORGANIZATION_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,
        organizationName: ORGANIZATION_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,
  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 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 "Skripterstellung mit der REST-API und 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,
  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 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 "Variablen."

(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 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();
})();

Testen des Skripts

Du kannst deinen Workflow manuell auslösen, um das Skript zu testen. Weitere Informationen finden Sie unter Manuelles Ausführen eines Workflows und unter Verwenden von Workflowausführungsprotokollen.

Alternative Methoden

In diesem Beispiel wurden GitHub Actions verwendet, um Anmeldeinformationen sicher zu speichern und das Skript nach einem Zeitplan auszuführen. Wenn du dieses Skript jedoch lieber auf deinem Server ausführen möchtest, das Webhookübermittlungen verarbeitet, kannst du Folgendes tun:

  • Speichere die Anmeldeinformationen auf eine andere sichere Weise, z. B. einen Geheimnis-Manager wie Azure Key Vault. Außerdem musst du das Skript aktualisieren, um von deinem neuen Speicherort aus auf die Anmeldeinformationen zuzugreifen.
  • Führe das Skript nach einem Zeitplan auf deinem Server aus, z. B. mithilfe eines Cron-Auftrags oder eines Aufgabenplaners.
  • Aktualisiere das Skript so, dass die letzte Laufzeit an einem Ort gespeichert wird, auf den dein Server zugreifen und den er aktualisieren kann. Wenn du dich entscheidest, die letzte Laufzeit nicht als GitHub Actions-Geheimnis zu speichern, kannst du die API-Aufrufe entfernen, um auf die Konfigurationsvariable zuzugreifen und sie zu aktualisieren.