Skip to main content

Livrer de nouveau de manière automatique les livraisons échouées pour un webhook de l'application GitHub

Vous pouvez écrire un script pour gérer les échecs de livraison d'un webhook GitHub App.

À propos des nouvelles livraisons automatiques qui n'ont pas abouti

Cet article décrit comment écrire un script pour trouver et effectuer de nouvelles livraisons automatiques qui n'ont pas abouti, pour un webhook GitHub App. Pour en savoir plus sur les livraisons échouées, reportez-vous à « Gestion des échecs de livraison de webhook ».

Cet exemple vous montre :

  • un script qui trouvera et effectuera de nouvelles livraisons automatiques qui n'ont pas abouti, pour un webhook GitHub App ;
  • les informations d'identification dont votre script aura besoin et la manière de les stocker en toute sécurité en tant que secrets GitHub Actions ;
  • un flux de travail GitHub Actions qui peut accéder en toute sécurité à vos informations d'identification et exécuter le script périodiquement.

Cet exemple utilise GitHub Actions, mais vous pouvez également exécuter ce script sur votre serveur qui gère les livraisons de webhooks. Pour en savoir plus, reportez-vous aux « méthodes alternatives ».

Stockage des informations d'identification pour le script

Les points de terminaison pour trouver et livrer de nouveau les webhooks qui n'ont pas abouti nécessitent un JSON web token, qui est généré à partir de l'ID d'application et de la clé privée de votre application.

Les points de terminaison permettant de récupérer et de mettre à jour la valeur des variables d'environnement nécessitent un jeton d'accès à l'installation personal access token, GitHub App ou un jeton d'accès à l'utilisateur GitHub App. Cet exemple utilise un personal access token. Si votre GitHub App est installé sur le référentiel où ce workflow sera exécuté et a l'autorisation d'écrire des variables de référentiel, vous pouvez modifier cet exemple pour créer un jeton d'accès d'installation pendant le workflow GitHub Actions au lieu d'utiliser un personal access token. Pour plus d’informations, consultez « Effectuer des requêtes d’API authentifiées avec une application GitHub dans un workflow GitHub Actions ».

  1. Trouvez l'ID d'application de votre GitHub App. Vous trouverez l’ID de l’application dans la page de paramètres de votre application. L’ID de l’application n’est pas l’ID client. Pour plus d’informations sur la navigation vers la page des paramètres pour votre GitHub App, consultez « Modification d’une inscription d’application GitHub ».

  2. Stockez l'identifiant de l'application obtenu à l'étape précédente en tant que GitHub Actions secret dans le référentiel où vous souhaitez que le flux de travail s'exécute. Pour plus d’informations sur le stockage de secrets, consultez « Utilisation de secrets dans GitHub Actions ».

  3. Générez une clé privée pour votre application. Pour en savoir plus sur la génération d'une clé privée, reportez-vous à « Gestion des clés privées pour les applications GitHub ».

  4. Stockez la clé privée, y compris -----BEGIN RSA PRIVATE KEY----- et -----END RSA PRIVATE KEY-----, à partir de l'étape précédente en tant que GitHub Actions secret dans le référentiel où vous souhaitez que le flux de travail s'exécute.

  5. Créez un personal access token avec l’accès suivant. Pour plus d’informations, consultez « Gestion de vos jetons d'accès personnels ».

    • Pour un fine-grained personal access token, octroyez au jeton :
      • L'accès en écriture à l'autorisation des variables du référentiel
      • Accès au référentiel dans lequel ce flux de travail s'exécutera
    • Pour un personal access token (classic), octroyez au jeton l’étendue repo.
  6. Stockez votre personal access token de l'étape précédente sous la forme d'un secret GitHub Actions dans le référentiel où vous souhaitez que le workflow s'exécute.

Ajout d'un flux de travail qui exécutera le script

Cette section montre comment vous pouvez utiliser un flux de travail GitHub Actions pour accéder en toute sécurité aux informations d'identification que vous avez stockées dans la section précédente, définir des variables d'environnement et exécuter périodiquement un script pour trouver et effectuer de nouvelles livraisons qui n'ont pas abouti

Copiez ce workflow GitHub Actions dans un fichier YAML dans l’annuaire .github/workflows du référentiel où vous souhaitez que le workflow s’exécute. Remplacez les espaces réservés à l’étape Run script, comme décrit ci-dessous.

YAML
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@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:
          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 with GITHUB_ or a number. For more information, see "Variables."
#
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@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_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

Ajout du script

Cette section montre comment écrire un script pour trouver et effectuer de nouvelles livraisons qui n'ont pas abouti.

Copiez ce script dans un fichier appelé .github/workflows/scripts/redeliver-failed-deliveries.js dans le même référentiel où vous avez enregistré le fichier de workflow GitHub Actions ci-dessus.

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

This script uses GitHub's Octokit SDK to make API requests. For more information, see "Écriture de scripts avec l’API REST et 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 "Écriture de scripts avec l’API REST et 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 "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 { 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();
})();

Test du script

Vous pouvez déclencher votre workflow manuellement pour tester le script. Pour plus d’informations, consultez « Exécution manuelle d’un workflow » et « Using workflow run logs ».

Autres méthodes

Cet exemple a utilisé GitHub Actions pour stocker en toute sécurité les informations d'identification et exécuter le script selon une planification. Cependant, si vous préférez exécuter ce script sur votre serveur qui gère les livraisons de webhooks, vous pouvez :

  • Stocker les informations d'identification d'une autre manière sécurisée, par exemple dans un gestionnaire de secret comme Azure Key Vault. Vous devrez également mettre à jour le script pour accéder aux informations d'identification à partir de leur nouvel emplacement.
  • Exécuter le script selon un calendrier sur votre serveur, par exemple en utilisant une tâche cron ou un planificateur de tâches.
  • Mettre à jour le script pour qu'il stocke la dernière heure d'exécution à un endroit auquel votre serveur peut accéder et qu'il peut mettre à jour. Si vous choisissez de ne pas stocker la dernière heure d'exécution en tant que secret GitHub Actions, vous n'avez pas besoin d'utiliser un personal access token, et vous pouvez supprimer les appels API pour accéder à la variable de configuration et la mettre à jour.